Skip to content

DNSSEC and Split-Horizon DNS

Standard DNS has no authentication. An attacker who can intercept DNS responses can return forged answers - redirecting mybank.com to a phishing site. This is DNS spoofing (also called DNS cache poisoning).

DNSSEC adds cryptographic signatures to DNS records. A resolver can verify that the data came from the authoritative server and hasn’t been tampered with.


DNSSEC builds a chain of trust from the root zone down to the authoritative zone:

Root Zone (.)
│ Signed by Root KSK (Root KSK is self-signed and published in trust anchors)
TLD Zone (.com)
│ DS record in root zone → validates .com DNSKEY
Domain Zone (example.com)
│ DS record in .com zone → validates example.com DNSKEY
Resource Records (A, MX, AAAA, etc.)
RRSIG record → signature over the resource records
RecordPurpose
DNSKEYZone’s public key(s). Two types: ZSK (Zone Signing Key) and KSK (Key Signing Key)
RRSIGCryptographic signature over a set of resource records
DS (Delegation Signer)Hash of child zone’s KSK, stored in parent zone. Creates the chain link
NSEC / NSEC3Proves a domain name does NOT exist (authenticated denial of existence)
Resolver wants example.com A record:
1. Gets A record + RRSIG from example.com nameserver
2. Gets DNSKEY for example.com zone
3. Verifies RRSIG matches A record, signed by ZSK in DNSKEY
4. Gets DS record for example.com from .com zone
5. Verifies DS is hash of example.com DNSKEY (KSK)
6. Gets DNSKEY for .com zone
7. Verifies .com DNSKEY matches DS signed by root zone
8. Trusts root zone based on built-in trust anchor
Terminal window
# Check if a domain has DNSSEC enabled
dig example.com +dnssec
# Check DNSKEY records
dig DNSKEY example.com
# Check DS record in parent zone
dig DS example.com @a.gtld-servers.net
# Test full DNSSEC validation
dig +sigchase example.com
# (requires BIND's dig with +sigchase support)
# Or use the online DNSSEC debugger:
# https://dnssec-debugger.verisignlabs.com/
# Check if a resolver validates DNSSEC
dig sigok.verteiltesysteme.net +dnssec
# If you get SERVFAIL, resolver validates (this domain has a deliberately broken sig)
# If you get an answer, resolver does NOT validate

Split-horizon DNS (also called split-brain or split-view DNS) means the same domain name returns different answers depending on who’s asking.

External DNS (Internet)
query: api.example.com
answer: 203.0.113.10 (public load balancer)
Internal DNS (Corporate network)
query: api.example.com
answer: 10.0.1.50 (directly to internal service)
Use CaseWhy
Direct internal accessEmployees access internal app via internal IP, avoiding NAT hairpin
Hairpin NAT avoidanceWithout split-horizon, internal traffic goes Internet→Firewall→back in (slow, expensive)
SecurityInternal hosts see more services than external hosts (e.g., internal admin panels)
Dev/staging environmentsapp.example.com → production externally, staging server internally
Cloud migrationDuring cutover, internal sees new host, external still sees old
┌─────────────────────┐
│ Internal DNS │
Internet users ──X──│ (Active Directory │◀── Employees
(can't reach) │ or BIND) │ (internal clients)
└─────────────────────┘
api.example.com → 10.0.1.50
┌─────────────────────┐
Internet users ────▶│ Public DNS │
(8.8.8.8, etc.) │ (Cloudflare, │
│ Route53, etc.) │
└─────────────────────┘
api.example.com → 203.0.113.10

Implementing Split-Horizon with BIND (named)

Section titled “Implementing Split-Horizon with BIND (named)”
/etc/named.conf
# Define which networks are "internal"
acl "internal" {
10.0.0.0/8;
172.16.0.0/12;
192.168.0.0/16;
localhost;
localnets;
};
# Two views: internal and external
view "internal" {
match-clients { internal; }; # only respond to internal clients
zone "example.com" IN {
type master;
file "/etc/named/internal/example.com.zone"; # internal records
};
};
view "external" {
match-clients { any; }; # all other clients
zone "example.com" IN {
type master;
file "/etc/named/external/example.com.zone"; # public records
};
};

Implementing with systemd-resolved (Linux clients)

Section titled “Implementing with systemd-resolved (Linux clients)”
Terminal window
# Point a specific domain to an internal DNS server
# Useful for accessing internal corp domains from personal machine via VPN
# /etc/systemd/resolved.conf.d/internal.conf:
[Resolve]
DNS=10.0.0.53
Domains=~corp.example.com # only resolve corp.example.com internally
# Apply
sudo systemctl restart systemd-resolved
# Verify routing
resolvectl query app.corp.example.com
PitfallWhat happensFix
VPN clients use external DNSInternal names don’t resolve through VPNConfigure VPN to push internal DNS and search domains
Cache poisoning of internal resolverAttacker injects bad records into split-horizon resolverDNSSEC on internal zone, TSIG for zone transfers
Dev forgets split-horizon existsTests with external DNS, fails in split-horizon envDocument clearly; use /dev/hosts or .override files for dev
Zone sync lagInternal and external zones driftAutomate sync; use the same zone file with views filtering records