CoreDNS ile Split Horizon DNS Yapılandırması

Prodüksiyonda DNS yönetimi, kulağa basit gelen ama iş karmaşık ortamlara gelince sizi gerçekten zorlayan bir alan. Özellikle hem iç ağ hem dış dünyaya hizmet veren bir altyapınız varsa, aynı domain adının farklı IP adreslerini döndürmesini istediğiniz durumlar kaçınılmaz hale geliyor. İşte tam burada Split Horizon DNS devreye giriyor ve CoreDNS bu işi son derece zarif bir şekilde çözüyor.

Bu yazıda gerçek bir senaryoyu ele alacağız: sirket.local ve sirket.com domainleri için iç ağdan gelen sorgulara farklı, dış ağdan gelen sorgulara farklı yanıt veren bir CoreDNS yapısı kuracağız. Kubernetes ortamı için de özel bir bölüm var, o kısmı atlamayın.

Split Horizon DNS Nedir ve Neden CoreDNS?

Split Horizon, ya da bazı kaynaklarda “Split-Brain DNS” olarak da geçen bu yaklaşım, aynı DNS adının sorgulandığı konuma göre farklı yanıtlar döndürmesi üzerine kuruludur. Klasik senaryo şöyle: app.sirket.com adresi dışarıdan sorgulandığında load balancer’ınızın public IP’sini döndürür, iç ağdan sorgulandığında ise direkt olarak uygulama sunucusunun iç IP’sini. Bu sayede iç trafik gereksiz yere internete çıkıp geri dönmez, gecikme azalır, güvenlik artar.

BIND ile bu işi yıllarca yaptım. Çalışıyor, stabil, ama konfigürasyon yönetimi bir süre sonra iç karartıcı bir hal alıyor. CoreDNS ise Go ile yazılmış, plugin tabanlı, cloud-native bir DNS sunucusu. Kubernetes’in varsayılan DNS çözümleyicisi olması boşuna değil. Corefile syntax’ı net, anlaşılır ve versiyon kontrolüne çok daha kolay entegre oluyor.

Ortam Hazırlığı ve Kurulum

Anlatacaklarımı Ubuntu 22.04 üzerinde test ettim ama RHEL/Rocky Linux için de gerekli notları düşeceğim.

# CoreDNS'in son sürümünü indirin
# https://github.com/coredns/coredns/releases adresinden güncel versiyona bakın
wget https://github.com/coredns/coredns/releases/download/v1.11.1/coredns_1.11.1_linux_amd64.tgz
tar -xvzf coredns_1.11.1_linux_amd64.tgz
sudo mv coredns /usr/local/bin/
sudo chmod +x /usr/local/bin/coredns

# Servis kullanıcısı ve dizin yapısı
sudo useradd -r -s /bin/false coredns
sudo mkdir -p /etc/coredns/zones
sudo chown -R coredns:coredns /etc/coredns

RHEL tabanlı sistemlerde SELinux ile uğraşmak gerekebilir. CoreDNS binary’sine ve konfigürasyon dizinine doğru SELinux context’i atamayı unutmayın:

# RHEL/Rocky Linux için SELinux context ayarı
sudo semanage fcontext -a -t bin_t '/usr/local/bin/coredns'
sudo restorecon -v /usr/local/bin/coredns

# 53 portunu non-root kullanıcı için açmak
sudo setcap cap_net_bind_service=+ep /usr/local/bin/coredns

Systemd servis dosyasını oluşturalım:

sudo tee /etc/systemd/system/coredns.service > /dev/null <<EOF
[Unit]
Description=CoreDNS DNS Server
Documentation=https://coredns.io
After=network.target

[Service]
User=coredns
Group=coredns
ExecStart=/usr/local/bin/coredns -conf /etc/coredns/Corefile
Restart=on-failure
RestartSec=5
LimitNOFILE=1048576

# Güvenlik sertleştirme
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/etc/coredns

[Install]
WantedBy=multi-network.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable coredns

Corefile Yapısı ve Split Horizon Mantığı

CoreDNS’in çalışma mantığını kavramak için önce Corefile syntax’ını anlamak şart. Her “server block” hangi zone’ı, hangi portta dinleyeceğini tanımlar. Split Horizon için kritik olan view plugin’i değil (o ticari versiyonda), bunun yerine farklı IP adreslerini dinleyen ayrı server block’ları ve acl plugin kombinasyonunu kullanmak.

Senaryo şu: İki network arayüzümüz var. eth0 internal ağa bakıyor (192.168.1.0/24), eth1 ise dış yönlendiriciye. İç kullanıcılar için app.sirket.com -> 10.10.1.50 döndüreceğiz, dış dünya için aynı adres -> 203.0.113.10 döndürecek.

Temel Corefile iskeletini oluşturalım:

# /etc/coredns/Corefile

# İÇ AĞ için server block - sadece internal interface'i dinler
(internal_common) {
    log
    errors
    health 192.168.1.1:8080
    ready
    prometheus 192.168.1.1:9153
}

# DIŞ AĞ için server block - sadece external interface'i dinler  
(external_common) {
    log
    errors
}

# Internal network - sirket.com zone'u
sirket.com:53 {
    import internal_common
    
    # Sadece iç ağ sorgularını kabul et
    acl {
        allow net 192.168.0.0/16
        allow net 10.0.0.0/8
        allow net 172.16.0.0/12
        block
    }
    
    file /etc/coredns/zones/internal/sirket.com.db
    
    # İç ağda çözülemeyen sorgular için fallback
    forward . 8.8.8.8 8.8.4.4
    cache 300
}

# External network - aynı domain, farklı zone dosyası
sirket.com:5353 {
    import external_common
    file /etc/coredns/zones/external/sirket.com.db
    cache 3600
}

Burada bir gerçeklik paylaşmak istiyorum: CoreDNS’de aynı port üzerinde view benzeri bir IP tabanlı ayrım yapmak için ya iki ayrı port kullanıyorsunuz (ve önüne bir yönlendirici koyuyorsunuz) ya da iki ayrı IP üzerinde aynı portu dinliyorsunuz. Ben genellikle ikinci yöntemi tercih ediyorum.

# Gerçek ortam için önerilen yaklaşım
# eth0: 192.168.1.1 (internal)
# eth1: 203.0.113.1 (external)

sirket.com:53 {
    bind 192.168.1.1
    file /etc/coredns/zones/internal/sirket.com.db
    log
    errors
    cache 300
    acl {
        allow net 192.168.0.0/16
        allow net 10.0.0.0/8
        block
    }
}

sirket.com:53 {
    bind 203.0.113.1
    file /etc/coredns/zones/external/sirket.com.db
    log
    errors
    cache 3600
}

# Diğer tüm sorgular için upstream
. {
    forward . 8.8.8.8 1.1.1.1
    cache 300
    log
}

Zone Dosyalarını Oluşturmak

Şimdi asıl iş. İki ayrı zone dosyası oluşturacağız.

# Dizin yapısını hazırlayalım
sudo mkdir -p /etc/coredns/zones/internal
sudo mkdir -p /etc/coredns/zones/external

Internal zone dosyası – burada özel IP’leri kullanıyoruz:

; /etc/coredns/zones/internal/sirket.com.db
$ORIGIN sirket.com.
$TTL 300

@   IN  SOA ns1.sirket.com. hostmaster.sirket.com. (
            2024010101  ; Serial
            3600        ; Refresh
            900         ; Retry
            604800      ; Expire
            300 )       ; Minimum TTL

; Name servers
@       IN  NS  ns1.sirket.com.
@       IN  NS  ns2.sirket.com.

ns1     IN  A   192.168.1.1
ns2     IN  A   192.168.1.2

; Internal A records - özel IP'ler
@       IN  A   10.10.1.10
www     IN  A   10.10.1.10
app     IN  A   10.10.1.50
api     IN  A   10.10.1.51
db      IN  A   10.10.2.10
mail    IN  A   10.10.1.20

; İç ağa özgü servisler - dışarıdan erişilemeyen
gitlab  IN  A   10.10.3.10
jenkins IN  A   10.10.3.11
grafana IN  A   10.10.3.20
kibana  IN  A   10.10.3.21

; MX records
@       IN  MX  10  mail.sirket.com.

External zone dosyası – dışarıya açık public IP’ler:

; /etc/coredns/zones/external/sirket.com.db
$ORIGIN sirket.com.
$TTL 3600

@   IN  SOA ns1.sirket.com. hostmaster.sirket.com. (
            2024010101  ; Serial
            86400       ; Refresh
            7200        ; Retry
            604800      ; Expire
            3600 )      ; Minimum TTL

; Name servers - public IP'ler
@       IN  NS  ns1.sirket.com.
@       IN  NS  ns2.sirket.com.

ns1     IN  A   203.0.113.1
ns2     IN  A   203.0.113.2

; External A records - public IP'ler
@       IN  A   203.0.113.10
www     IN  A   203.0.113.10
app     IN  A   203.0.113.10
api     IN  A   203.0.113.11

; Mail sunucusu
mail    IN  A   203.0.113.20
@       IN  MX  10  mail.sirket.com.

; TXT records (SPF, DKIM vb.)
@       IN  TXT "v=spf1 ip4:203.0.113.20 ~all"

Kubernetes Ortamında Split Horizon

Kubernetes kullanıyorsanız, CoreDNS zaten cluster’ınızda çalışıyor ve ConfigMap üzerinden yönetiliyor. Buradaki senaryo biraz farklı: cluster içindeki pod’ların hem .cluster.local zone’unu hem de dış domainleri düzgün çözmesini, ama iç servislere erişirken iç IP’leri kullanmasını istiyorsunuz.

# Mevcut CoreDNS ConfigMap'i alın
kubectl get configmap coredns -n kube-system -o yaml > coredns-configmap-backup.yaml

# Düzenlemek için
kubectl edit configmap coredns -n kube-system

Kubernetes için önerilen ConfigMap yapısı:

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        
        # Kubernetes cluster.local zone'u
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        
        # İç domain için özel forward - kendi DNS sunucunuza
        # sirket.local sorgularını iç DNS'e yönlendir
        forward sirket.local 192.168.1.1 {
            policy sequential
            health_check 5s
        }
        
        # sirket.com için de iç DNS sunucusunu kullan
        # (Split Horizon'dan internal cevaplar gelecek)
        forward sirket.com 192.168.1.1 {
            policy sequential
            health_check 5s
        }
        
        # Geri kalan her şey için public DNS
        forward . 8.8.8.8 1.1.1.1 {
            max_concurrent 1000
        }
        
        prometheus :9153
        cache 30
        loop
        reload
        loadbalance
    }

Bu ConfigMap değişikliğini uyguladıktan sonra CoreDNS pod’larının otomatik reload etmesini bekleyin. reload plugin sayesinde genellikle pod restart’a gerek kalmıyor.

ACL ve Güvenlik Sertleştirmesi

Split Horizon kurulumunda güvenlik kritik. Yanlış yapılandırma, iç ağ bilgilerinin dışarıya sızmasına neden olabilir. ACL plugin’ini detaylı ele alalım:

sirket.com:53 {
    bind 192.168.1.1
    
    acl {
        # RFC 1918 private adreslere izin ver
        allow net 10.0.0.0/8
        allow net 172.16.0.0/12
        allow net 192.168.0.0/16
        
        # IPv6 link-local
        allow net fe80::/10
        
        # Loopback her zaman izin
        allow net 127.0.0.0/8
        allow net ::1/128
        
        # Geri kalan her şeyi engelle
        block
    }
    
    file /etc/coredns/zones/internal/sirket.com.db
    log . "{remote} - {type} {name} {class} {proto} {size} {rcode} {duration}"
    errors
    cache 300
}

İzleme ve Debug

Prodüksiyona almadan önce her şeyin doğru çalıştığını test etmek için kullandığım komutlar:

# CoreDNS config syntax kontrolü
coredns -conf /etc/coredns/Corefile -dns.port 1053 &

# İç ağ sorgusunu simüle et - internal IP'yi sorgula
dig @192.168.1.1 app.sirket.com A

# Dış ağ sorgusunu simüle et
dig @203.0.113.1 app.sirket.com A

# Zone transfer deneyin - engellenmeli
dig @192.168.1.1 sirket.com AXFR

# Prometheus metrikleri kontrol
curl -s http://192.168.1.1:9153/metrics | grep coredns_dns_requests_total

# CoreDNS loglarını takip et
sudo journalctl -u coredns -f

# Kubernetes'te CoreDNS pod logları
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=100 -f

Gerçek bir prodüksiyon ortamında karşılaştığım klasik sorun: Zone dosyasında serial numarayı güncellemeyi unutmak. CoreDNS file plugin’i default olarak 60 saniyede bir zone dosyasını kontrol eder ama cache nedeniyle değişiklikler hemen yansımayabilir. Serial numarayı her değişiklikte artırmak zorunlu, aksi halde secondary DNS sunucuları (varsa) güncel zone’u almaz.

# Zone dosyasını değiştirdikten sonra reload zorlamak için
sudo systemctl reload coredns
# veya
sudo kill -SIGUSR1 $(pgrep coredns)

# Zone'un doğru yüklenip yüklenmediğini test et
dig @localhost version.bind TXT CH +short

Yüksek Erişilebilirlik için İki Düğümlü Yapı

Tek bir CoreDNS instance’ı prodüksiyonda yeterli değil. En basit HA yaklaşımı iki sunucu, keepalived ile VIP yönetimi:

# Her iki sunucuya da CoreDNS kurun, sonra keepalived
sudo apt install keepalived -y

# Master sunucu için keepalived.conf
sudo tee /etc/keepalived/keepalived.conf > /dev/null <<EOF
vrrp_script check_coredns {
    script "dig @127.0.0.1 health.coredns.local +time=2 +tries=1 || exit 1"
    interval 5
    fall 2
    rise 2
}

vrrp_instance DNS_VIP {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    
    authentication {
        auth_type PASS
        auth_pass dns-secret-2024
    }
    
    virtual_ipaddress {
        192.168.1.100/24
    }
    
    track_script {
        check_coredns
    }
}
EOF

sudo systemctl enable keepalived --now

Bu yapıda istemciler her zaman 192.168.1.100 adresine sorgu atar. Master düştüğünde keepalived VIP’i backup sunucuya taşır, kesinti birkaç saniyeyle sınırlı kalır.

Sık Karşılaşılan Sorunlar

Prodüksiyonda gördüğüm ve zaman kaybettiren hatalar:

SERVFAIL yanıtları: Zone dosyasındaki syntax hatası en yaygın sebep. named-checkzone aracıyla zone dosyasını CoreDNS bağımsız olarak doğrulayabilirsiniz.

Yanlış bind adresi: CoreDNS default olarak tüm interface’leri dinler. bind direktifini unutursanız internal zone’unuz dışarıya da yanıt verebilir.

Cache tutarsızlığı: İç ve dış cevaplar arasında geçiş yaparken test ederken +nocache flagini kullanın: dig @192.168.1.1 app.sirket.com +nocache

Kubernetes’te ndots sorunu: Cluster içi pod’lar kısa hostname’leri çözerken fazla sorgu atıyor olabilir. Pod spec’inde dnsConfig ile ndots değerini düşürmek yardımcı olur.

# Gerçek zamanlı sorgu sayısını izle
watch -n 2 "curl -s http://192.168.1.1:9153/metrics | grep 'coredns_dns_requests_total'"

# Hangi sorgular SERVFAIL alıyor?
sudo journalctl -u coredns | grep SERVFAIL | tail -20

Sonuç

CoreDNS ile Split Horizon kurulumu, doğru anlaşıldığında BIND’a göre çok daha yönetilebilir bir hal alıyor. bind direktifi ve zone dosyası ayrımı sayesinde iç-dış trafik yönetimi temiz kalıyor, Corefile’ın okunabilirliği konfigürasyon hatalarını azaltıyor.

Kritik noktalara dikkat çekmek istiyorum: Zone dosyalarının serial numaralarını ihmal etmeyin, ACL bloklarını daima “block” ile kapatın, ve Prometheus metriklerini mutlaka izleme sisteminize entegre edin. DNS sorunları sessiz sedasız yayılır, fark ettiğinizde yarım saatlik bir şey haline gelmiş olur.

Kubernetes ortamındakilere özellikle şunu söylemeliyim: CoreDNS ConfigMap’inizi GitOps akışınıza dahil edin. Elle yapılan değişiklikler cluster yeniden oluşturmalarında kaybolur ve “DNS neden çalışmıyor” sorusunun cevabını saat 03:00’da aramak istemezsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir