A hidden primary and dnsdist: hardening authoritative DNS
Why authoritative DNS needs a hidden primary, the dnsdist rules worth putting in front of every public nameserver, and how to check zones and reverse DNS.

Authoritative DNS is an amplifier for attackers and a frequent target of floods. Two design decisions do most of the work of making it resilient: keep the primary hidden, and put a DNS-aware load balancer in front of every public server. Here is how, with the rules we consider the minimum.
Hide the primary
The server where zones are edited, keys live and the master copy of data sits should never answer the internet. Instead, it replicates to public secondaries over an internal-only network, and only the secondaries appear in NS records.
That gives you three things. The most valuable server is not exposed to floods or exploits. Public servers become disposable: if one is attacked or fails, its peers carry on and it can be rebuilt from the primary. And you can spread secondaries across different networks, sites and platforms, so no single failure domain takes the domain down.
Whether replication is classic AXFR and NOTIFY or database replication underneath PowerDNS, the property to protect is the same: the primary's address must never appear in a public NS or SOA record. Check it:
dig +short NS example.com
dig +short SOA example.com # the MNAME field should not be your hidden primary
Why a generic load balancer is not enough
A TCP or UDP load balancer can spread queries, but it cannot read them. Almost every useful DNS defence depends on understanding the query: its type, the zone it asks about, whether it is an UPDATE, how many failures a source is generating. dnsdist, from the PowerDNS project, sits in front of the nameserver, holds the public port, and makes those decisions before the query reaches the backend. The authoritative server itself then listens only on localhost.
The dnsdist rules we consider the minimum
-- backend listens on localhost only
newServer({address="127.0.0.1:5300", name="auth"})
setLocal("0.0.0.0:53")
addLocal("[::]:53")
trusted = newNMG(); trusted:addMask("10.0.0.0/8") -- your internal replication network
ourZones = newSuffixMatchNode(); ourZones:add("example.com.")
-- ANY queries are an amplification tool; refuse them
addAction(QTypeRule(DNSQType.ANY), RCodeAction(DNSRCode.REFUSED))
-- no dynamic updates or notifies from the internet
addAction(AndRule({OpcodeRule(DNSOpcode.Update), NotRule(NetmaskGroupRule(trusted))}),
RCodeAction(DNSRCode.REFUSED))
addAction(AndRule({OpcodeRule(DNSOpcode.Notify), NotRule(NetmaskGroupRule(trusted))}),
RCodeAction(DNSRCode.REFUSED))
-- only answer for zones we actually serve
addAction(NotRule(SuffixMatchNodeRule(ourZones)), RCodeAction(DNSRCode.REFUSED))
-- cap queries per source address
addAction(MaxQPSIPRule(50), DropAction())
-- automatic temporary blocks for sources generating floods of failures
local dbr = dynBlockRulesGroup()
dbr:setRCodeRate(DNSRCode.NXDOMAIN, 50, 10, "NXDOMAIN flood", 60)
dbr:setRCodeRate(DNSRCode.REFUSED, 50, 10, "REFUSED flood", 60)
function maintenance() dbr:apply() end
What each rule buys you:
- Refusing ANY removes the classic amplification query.
- Refusing UPDATE and NOTIFY from outside closes the paths by which zone content or replication could be influenced from the internet.
- Refusing zones you do not serve before the backend sees them keeps random junk and reflection probes off the nameserver entirely.
- Set per-source rate caps from your real traffic, generously enough for large resolvers.
- Dynamic blocks on failure rates catch random-subdomain attacks, which generate floods of NXDOMAIN answers from many sources, without blocking a resolver for one bad minute.
Add response rate limiting on the backend or in dnsdist so that even legitimate-looking answers cannot be turned into a flood toward a spoofed address.
DNSSEC without key shuffling
If your secondaries sign zones themselves, every one of them needs the keys, and distributing keys is where DNSSEC deployments go wrong. When keys live in the same database that replicates to the secondaries, every server signs identically by construction. When you move a signed domain between providers, stage it in order: remove the old DS record at the registry, wait for caches to expire, sign on the new servers, then publish the new DS. Validation never breaks.
Reverse DNS is part of the job
Treat reverse DNS as a rule: every address assigned to an interface, service or host gets a PTR in the same change, and every reverse zone is delegated to all your nameservers. Check both directions and the parent delegation:
dig +short -x 192.0.2.25
dig +short host.example.com A
dig +short NS 2.0.192.in-addr.arpa
The forward and reverse names should agree, and the reverse zone's NS set should match what the regional registry has on file.
Watch the answers, not just the servers
A nameserver that is up but serving a stale zone is worse than one that is down, because nothing alarms. Monitor what matters: compare each zone's SOA serial across every server, compare record counts, watch replication lag, and alert if the hidden primary ever appears in public data. Checking from a few external vantage points that every public server answers authoritatively closes the loop.
Your checklist
- Is your primary hidden, and absent from NS and SOA?
- Are public servers behind a DNS-aware front end that refuses ANY, external UPDATE and NOTIFY, and unknown zones?
- Are there per-source limits and dynamic blocks for failure floods?
- Do all servers sign identically, and is your DS rollover procedure written down?
- Does every assigned address have a matching PTR?
- Do you alert on serial mismatch between servers?
We run our authoritative DNS on PowerDNS exactly this way, and we build dedicated clusters for customers who want their own. If you would like a review of your DNS, we would be glad to help.
Published 22 October 2026.
All field notes