DNS Architecture and Resilience

Design DNS infrastructure for reliability, performance, and security. Covers DNS resolution, record types, failover strategies, DNSSEC, DNS load balancing, GeoDNS, and the patterns that prevent DNS from being the single point of failure.

DNS Architecture and Resilience

DNS architecture and resilience covers how the domain name system is structured, how resolvers and authoritative services interact, and what breaks when the system is under load, misconfigured, or under attack. It matters whenever a service depends on name resolution—load balancers, service meshes, CI runners, outbound internet access—and an outage propagates latency or complete failure. This page is organized around the tasks an engineer typically needs to perform: diagnosing why a name won’t resolve, setting up a recursive resolver that won’t collapse under load, hardening the stack against the most common silent failure modes, and managing zone authority across multiple networks. Every command, flag, and error text shown here comes from shipping, publicly documented tools. No vendor‑specific internals are embedded.

Diagnose a resolution failure

When a service can’t reach a host by name, start by probing the full resolution chain. The dig utility is the standard tool for this; it is shipped with BIND distributions and works on any Linux, macOS, or Windows system with a resolver library.

$ dig @8.8.8.8 google.com A +short
142.250.185.238

The +short flag strips all metadata and returns only the answer section. If the query returns nothing, check whether the resolver is reachable at all:

$ dig @127.0.0.1 google.com A +short
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 0x9a2b

The status: line in the ;; section reports the RCODE. The most common values you’ll see:

RCODEMeaning
NOERRORQuery succeeded; may return empty answer if the name doesn’t exist
SERVFAILServer encountered an internal error (often a malformed zone, exhausted resources, or DNSSEC validation failure)
REFUSEDServer deliberately refuses the query (commonly due to ACLs or rate‑limiting)
NXDOMAINThe domain name does not exist in the zone
FORMERRThe name server could not interpret the query (rare, usually a configuration bug)

To trace the path from the root down, use the +trace flag. This queries each level of the hierarchy and shows the NS records handed down at each step:

$ dig google.com +trace | head -20
; <<>> DiG 9.18.3 <<>> google.com +trace
;; global options: +cmd
. 54300 IN NS a.root-servers.net.
. 54300 IN NS b.root-servers.net.
...
com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
...
google.com. 21566 IN NS ns1.google.com.
google.com. 21566 IN NS ns2.google.com.

If a SERVFAIL appears at any level, the next step is to verify that the local resolver is actually listening and not blocked by a firewall or mis‑configured systemd‑resolved instance.

$ systemctl status systemd-resolved
● systemd-resolved.service - Network Name Resolution
   Loaded: loaded (/usr/lib/systemd/system/systemd-resolved.service; enabled; vendor preset: enabled)
   Active: active (running) since ...

If the service is inactive, restart it and verify that the symlink /etc/resolv.conf points to the resolved control file:

$ sudo systemctl restart systemd-resolved
$ readlink /etc/resolv.conf
/run/systemd/resolve/stub-resolv.conf

A broken or stale symlink is a frequent cause of “resolver not found” errors after a network namespace change or container restart.

Configure a resilient recursive resolver

A recursive resolver sits between clients and the rest of the DNS hierarchy. Its job is to cache responses, forward queries when needed, and protect the client from repeated root‑level queries. Two widely deployed implementations are systemd-resolved (default on most modern Linux desktops and servers) and dnsmasq (common in LANs, containers, and embedded devices).

systemd‑resolved

On systems using systemd, the resolver state is managed via resolvectl. To view the current configuration:

$ resolvectl status

To set custom forwarders (e.g., for a split‑horizon setup) on interface eth0:

$ sudo resolvectl dns eth0 1.1.1.1 8.8.8.8

To adjust the cache size—the number of recent queries kept in memory—edit the transient unit override (this does not require a full system restart):

$ sudo systemctl edit systemd-resolved.service

Add the following line in the [Service] section:

Environment=RESOLVED_CACHE_SIZE=10000

Then reload and restart:

$ sudo systemctl daemon-reload
$ sudo systemctl restart systemd-resolved

The RESOLVED_CACHE_SIZE environment variable is documented in the systemd-resolved man page; values are in entries, not bytes. A typical production deployment uses a cache between 5 000 and 50 000 entries, large enough to absorb bursty traffic but small enough to bound memory usage.

dnsmasq

dnsmasq combines a DHCP server, a DNS cache, and a small authoritative zone server. It is frequently deployed inside Kubernetes clusters, virtual networks, and on‑premises LANs. A minimal production‑grade config snippet:


cache-size=1000
log-queries
bind-interfaces
port=53

The cache-size directive accepts an integer; 0 disables caching entirely (not recommended for production). The log-queries flag writes every query to /var/log/dnsmasq, which is invaluable during debugging but should be disabled in high‑throughput environments because of I/O load.

To forward all queries for a specific domain to an internal resolver:

server=/internal.example.com/10.0.0.1#53

After saving the file, reload the service:

$ sudo systemctl restart dnsmasq

Verify that the cache is active and that the forwarder is being used:

$ dig @127.0.0.1 google.com A +short
; answer comes from 127.0.0.1 (dnsmasq), forwarded to 1.1.1.1

If you need to limit the EDNS buffer size (to avoid fragmentation on paths with small MTU), use the --max-udp-size flag:

$ sudo systemctl edit dnsmasq.service
# Add:
Environment=DNSMASQ_MAX_UDP_SIZE=1232
$ sudo systemctl daemon-reload
$ sudo systemctl restart dnsmasq

The value 1232 is the default maximum EDNS payload size for many implementations; lowering it can prevent SERVFAIL responses on networks that drop packets larger than 512 bytes.

Harden against amplification, cache poisoning & EDNS issues

Response Rate Limiting (RRL)

Amplification attacks flood a target with DNS responses forged to appear as if they came from your resolver. Response Rate Limiting throttles repeated queries for the same name, returning a truncated response that forces the client to fall back to TCP or retry later. Most modern resolvers include RRL either compiled in or via a configuration flag.

In unbound, RRL is enabled in the config file:

server:
    rrl: yes
    rrl-log-level: 1
    rrl-prefix: "0.0.0.0/0"

In dnsmasq, there is no built‑in RRL, so rate‑limiting must be performed upstream (firewall, load balancer, or a side‑car proxy). The sharp edge here is that without RRL, a single malicious query can trigger thousands of upstream lookups, quickly exhausting both outbound bandwidth and the resolver’s own cache.

EDNS buffer size and fragmentation

When a resolver sends a query with an EDNS option requesting a large payload (e.g., DNSSEC records, DNS‑based service discovery), and the network path drops packets larger than the interface MTU, the authoritative server will respond with a truncated flag (TC=1), prompting the client to retry over TCP. This adds latency and can break clients that do not implement TCP fallback gracefully.

To inspect the current buffer size advertised by a resolver:

$ dig google.com +bufsize=4096 +subnet=0.0.0.0/0

The +bufsize flag tells the server the maximum UDP payload it should accept. If the server responds with status: NOERROR but the answer section is empty or truncated, the client must fall back to TCP. In production, it is common to see +bufsize=1232 as a safe default; values above 4096 often trigger SERVFAIL on middleboxes that perform deep packet inspection.

Cache poisoning and ID randomization

DNS cache poisoning relies on an attacker guessing the 16‑bit transaction ID and the 32‑bit UDP source port, then injecting a forged answer that the resolver caches. Modern resolvers mitigate this by randomizing both the ID and the source port, and by validating DNSSEC signatures when they are present.

A REFUSED response from a resolver that previously returned NOERROR for the same name can signal that the resolver has detected an inconsistency—often a sign of a poisoning attempt or a zone transfer that introduced duplicate records. To force a resolver to flush its cache and re‑query from scratch:

$ sudo resolvectl flush-caches

or, for dnsmasq:

$ sudo dnsmasq --cache-size=0   # temporarily disable cache, then re‑enable
$ sudo systemctl restart dnsmasq

After restart, verify that the resolver returns the correct answer without relying on stale cache entries.

Manage zone authority, replication & split‑horizon

Zone transfers

Authoritative servers need a way to propagate changes to secondary servers. The standard mechanism is an AXFR (zone transfer) over TCP. To initiate a transfer from a secondary:

$ dig example.com AXFR @ns1.example.com

The output lists every record in the zone. If the command returns XFR: failure, check that:

A common misconfiguration is listing allow-transfer { any; }; on the primary during testing, then forgetting to tighten it for production. The sharp edge is that an overly permissive allow-transfer can be abused to enumerate every record in your zone, including internal hostnames and IP ranges.

Split‑horizon DNS

Split‑horizon (also called “split‑brain” DNS) serves different answers to internal vs. external clients. This is typically achieved by running two resolver instances bound to different interfaces, or by using dnsmasq with conditional forwarders.

Example: internal clients on 10.0.0.0/8 should resolve internal.example.com to 10.0.0.5, while external clients see 198.51.100.23.

In dnsmasq, add:

server=/internal.example.com/10.0.0.1#53
local=/internal.example.com

and ensure the DHCP option on the internal network points at the dnsmasq instance listening on the eth1 interface:

$ sudo ip addr add 10.0.0.1/24 dev eth1
$ sudo systemctl restart dnsmasq

External clients hitting the public IP of your DNS service will use the authoritative server’s default zone, which does not contain the internal.example.com record, so they receive the public address.

To test the two views from the same machine, use @ to point dig at specific interfaces:

$ dig @10.0.0.1 internal.example.com A +short
10.0.0.5

$ dig @8.8.8.8 internal.example.com A +short
; referral or NXDOMAIN—no internal record

If the external query returns NXDOMAIN, that is expected; the name simply does not exist in the public zone.

rndc for BIND‑based authoritative servers

If you operate a BIND server, rndc (remote name daemon control) is the standard administrative interface. Common operations:

$ sudo rndc status          # show server state, loaded zones, connections
$ sudo rndc flush           # clear the server’s cache and pending transactions
$ sudo rndc reload          # reload zone files from disk
$ sudo rndc status          # confirm the reload completed without errors

A frequent production issue is rndc: connection refused, which means the rndc channel is not allowed in /etc/bind/rndc.conf or the allow-query/allow-transfer ACLs are too restrictive. The fix is to add an key block and reference it in the controls section of named.conf, then restart BIND.

Sharp edges — what silently fails, what you only learn after it breaks

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.