PowerDNS hidden primary with Knot secondaries, the secure way
A hidden primary is only hidden until someone reads your NS records, your NOTIFY traffic or your leaked TSIG key. PowerDNS makes the database side pleasant; keeping it out of reach is your job.
The short answer
Keep the PowerDNS primary on a private transfer network that only the Knot secondaries can reach. List only the secondaries in NS. Require TSIG for every AXFR and NOTIFY, set allow-axfr-ips and only-notify to empty, set SOA-EDIT so signature changes reach the secondaries, and turn on dnssec-validation in Knot.
On this page
What goes wrong
A hidden primary is the server where you edit zones. It does not answer public queries. Public secondaries copy the zone from it by zone transfer (AXFR or IXFR) and serve it to the world. If the primary is not reachable, an attacker cannot flood it, exploit its API, or pull the full zone.
In practice the primary leaks in three ways.
First, it sits on a public address and relies on an ACL. An ACL is only as good as the TSIG key behind it. In PowerDNS, a request signed with an allowed TSIG key skips the IP checks entirely. Anyone with a copy of the key can transfer the zone from anywhere that can reach port 53.
Second, PowerDNS sends NOTIFY to every name server in the NS set by default. The NS set is public. So is any address your primary sends NOTIFY from.
Third, signatures go stale on the secondaries. PowerDNS signs answers live and rolls signatures every week, but it does not change the SOA serial when it does. A non-PowerDNS secondary like Knot sees the same serial, does not transfer, and keeps serving old signatures until they expire. Then the zone is bogus everywhere at once.
What the docs say
This setting only applies to AXFR without TSIG keys. If you allow a TSIG key to perform an AXFR, this setting will not be checked for that transfer, and the client will be able to perform the AXFR from everywhere.
Source: PowerDNS docs, allow-axfr-ips
With PowerDNS in Live-signing mode, the SOA serial is not increased by default when the RRSIG dates are rolled.
Source: PowerDNS docs, SOA-EDIT: ensure signature freshness on secondaries
The default is to notify the world.
Source: PowerDNS docs, only-notify
When a RRSIG expires on a secondary server, the whole zone is expired (permanent SERVFAIL until resolved).
Source: Knot DNS 3.5 reference, dnssec-validation
The PowerDNS docs warn about TSIG and IP checks, but do not say the obvious
consequence: the only reliable hidden primary is one the internet cannot
route to. The test below shows that ALLOW-AXFR-FROM zone metadata is
skipped the same way. Neither project's docs describe this pairing end to
end.
The secure configuration
Network layout: the primary has one address, on a private transfer network. The secondaries have one address there and one public address.
transfer network (private) public
PowerDNS primary 203.0.113.10 <----> Knot 203.0.113.20 | 198.51.100.20 <---- resolversPowerDNS, /etc/powerdns/pdns.d/primary.conf:
primary=yes # send NOTIFY, serve AXFR
local-address=203.0.113.10 # transfer network only, never a public address
allow-axfr-ips= # no AXFR on IP address alone
only-notify= # do not NOTIFY whatever the NS records point to
also-notify=203.0.113.20 # notify the secondaries explicitly
default-soa-edit-signed=INCEPTION-INCREMENT # bump the served serial when RRSIGs roll weekly
version-string=anonymous
webserver=no # if you need the API, see the page on zones in git
api=noZone setup on the primary:
pdnsutil zone load example.com example.com.zone
pdnsutil zone set-kind example.com primary
pdnsutil zone secure example.com # ECDSA P-256 CSK by default
pdnsutil zone set-nsec3 example.com '1 0 0 -' # RFC 9276 parameters
pdnsutil zone rectify example.com
pdnsutil tsigkey generate xfr-knot hmac-sha256 # one key per secondary set
pdnsutil tsigkey activate example.com xfr-knot primary
pdnsutil metadata set example.com ALLOW-AXFR-FROM 203.0.113.20/32 # defense in depth only
pdnsutil zone export-ds example.com # the DS for the registrarThe NS set lists only public secondaries. The primary never appears in it:
example.com. NS ns1.example.com. ; Knot, 198.51.100.20
example.com. NS ns2.example.net. ; Knot, another provider or regionKnot, /etc/knot/knot.conf on each secondary:
server:
user: knot:knot
listen: [ 198.51.100.20@53, 203.0.113.20@53 ]
key:
- id: xfr-knot # same name as on the primary
algorithm: hmac-sha256
secret: <base64 secret from pdnsutil>
remote:
- id: hidden_primary
address: 203.0.113.10@53
key: xfr-knot # every SOA check and AXFR/IXFR is signed
acl:
- id: notify_from_primary
address: 203.0.113.10 # NOTIFY accepted only from the primary...
key: xfr-knot # ...and only when TSIG-signed
action: notify
zone:
- domain: example.com
master: hidden_primary
acl: notify_from_primary
dnssec-validation: on # refuse a transfer with broken or expired signatures
zonefile-sync: -1
# no transfer ACL: the secondary does not hand the zone to anyoneFirewall on the primary host, in addition to the network design:
# nftables: port 53 on the primary only from the secondaries' transfer addresses
nft add rule inet filter input ip saddr != { 203.0.113.20, 203.0.113.21 } udp dport 53 drop
nft add rule inet filter input ip saddr != { 203.0.113.20, 203.0.113.21 } tcp dport 53 dropProve it
The test (secure-tests/powerdns-hidden-primary-knot-secondaries/run.sh)
builds the layout above with two internal Docker networks.
The primary holds the signed zone, with TSIG and the IP metadata set:
pdnsutil zone show example.comThis is a Master zone
Last SOA serial number we notified: 0 != 2026092401 (serial in the database)
Zone has following allowed TSIG key(s): xfr-knot
Metadata items:
ALLOW-AXFR-FROM 203.0.113.20/32
NSEC3PARAM 1 0 0 -
SOA-EDIT-API DEFAULT
TSIG-ALLOW-AXFR xfr-knot
Zone has hashed NSEC3 semantics, configuration: 1 0 0 -
keys:
ID = 1 (CSK), flags = 257, tag = 54607, algo = 13, bits = 256 Active Published ( ECDSAP256SHA256 )
DS = example.com. IN DS 54607 13 2 8fa0222d0e807ce5e955f4e2f4db0de98b2951d123e503595c8962538043a5e9 ; ( SHA256 digest )Knot transfers with the key and validates every signature before serving. The database serial is 2026092401; SOA-EDIT serves 2026092402:
info: [example.com.] AXFR, incoming, remote 203.0.113.10@53 TCP, key xfr-knot., started
info: [example.com.] AXFR, incoming, remote 203.0.113.10@53 TCP, key xfr-knot., finished, remote serial 2026092402, 0.01 seconds, 3 messages, 1846 bytes
info: [example.com.] DNSSEC, validation successful, checked RRSIGs 9A public client gets the NS set without the primary, and a validated answer from the secondary:
kdig @198.51.100.20 example.com NS +norecurse +short
delv @198.51.100.20 -a anchor.conf +root=example.com www.example.com Ans1.example.com.
ns2.example.net.
; fully validated
www.example.com. 300 IN A 192.0.2.10
www.example.com. 300 IN RRSIG A 13 3 300 20261008000000 20260917000000 54607 example.com. KZUnrUnfn1xdxqxn5Ha6qCLlV8e+vV5xH77ZG45iB0BV8kCwnit/pZiF nV53OQrQd068rDw0Nz8ynjJ+wtU5dQ==A public client cannot reach the primary at all:
kdig @203.0.113.10 example.com SOA +norecurse +timeout=2 +retry=0;; WARNING: can't send query to 203.0.113.10@53(UDP)
;; ERROR: failed to query server 203.0.113.10@53(UDP)A host on the transfer network without the key is refused:
kdig @203.0.113.10 example.com AXFR;; ERROR: server replied with error 'NOTAUTH'The trap. The same host, 203.0.113.99, with a copy of the key. Neither
allow-axfr-ips (empty) nor ALLOW-AXFR-FROM (only 203.0.113.20) stops it:
kdig @203.0.113.10 example.com AXFR -y hmac-sha256:xfr-knot:<secret> | tail -3
docker logs hp-pdns | grep AXFR-out | tail -3;; Received 1846 B (3 messages, 20 records)
AXFR-out zone 'example.com', client '203.0.113.99:43571', denied: client IP has no permission
AXFR-out zone 'example.com', client '203.0.113.99:43571', failed: client may not request AXFR
AXFR-out zone 'example.com', client '203.0.113.99:45447', transfer initiatedThis is why the network, not the ACL, keeps the primary hidden.
An unsigned NOTIFY to the secondary is refused:
dig @198.51.100.20 example.com SOA +opcode=notify +norecurse;; ->>HEADER<<- opcode: NOTIFY, status: NOTAUTH, id: 38374A change on the primary reaches the secondary by signed NOTIFY and IXFR:
pdnsutil rrset add example.com api.example.com A 300 192.0.2.20
pdnsutil zone increase-serial example.com
pdns_control notify example.comAdded example.com to queue
info: [example.com.] IXFR, incoming, remote 203.0.113.10@53 TCP, key xfr-knot., receiving AXFR-style IXFR
info: [example.com.] AXFR, incoming, remote 203.0.113.10@53 TCP, key xfr-knot., started
info: [example.com.] AXFR, incoming, remote 203.0.113.10@53 TCP, key xfr-knot., finished, remote serial 2026092403, 0.05 seconds, 3 messages, 2159 bytes
info: [example.com.] refresh, remote 203.0.113.10@53, key xfr-knot., zone updated, 0.07 seconds, serial 2026092402 -> 2026092403, expires in 1209600 secondskdig @198.51.100.20 api.example.com A +dnssec +norecurse +noall +answerapi.example.com. 300 IN A 192.0.2.20
api.example.com. 300 IN RRSIG A 13 3 300 20261008000000 20260917000000 54607 example.com. 7KHWnZkBATrLlaPc54vJyaLR4lg0VMWRnLi6KxPBo8tO5+pc8eGLkkX7nB+sZ1J8bLNmzZQ2Ht3qbuIsOwpc2w==Mistakes people make
Trusting the ACL to hide the primary
allow-axfr-ips and ALLOW-AXFR-FROM do not apply to TSIG-signed
requests. A leaked key turns a "hidden" primary on a public address into a
public zone dump. Put the primary on a network the internet cannot reach,
and firewall port 53 to the secondaries.
Listing the primary in the NS set or the SOA MNAME
Resolvers query every NS. If the primary is in the NS set, it is not hidden. The SOA MNAME field is also public; point it at a public secondary name, as in the example zone.
Leaving only-notify at its default
The default notifies every address the NS names resolve to. With
only-notify= empty and explicit also-notify, the primary talks only to
your secondaries.
Forgetting SOA-EDIT with Knot secondaries
PowerDNS secondaries detect new signatures on their own. Knot does not. Set
default-soa-edit-signed (or SOA-EDIT per zone) so the served serial
changes when signatures roll, or the secondaries serve expired signatures.
One TSIG key for everything
The transfer key, the NOTIFY key and any update key for automation should be different keys. A key used by a CI job for DNS updates must not also allow full transfers. See the TSIG page for rotation.
Checklist
- Put the PowerDNS primary on a private transfer network with no public address.
- Firewall port 53 on the primary to the secondaries' transfer addresses.
- Set
allow-axfr-ips=andonly-notify=to empty; setalso-notifyto the secondaries. - Create one
hmac-sha256TSIG key for transfers and activate it withpdnsutil tsigkey activate <zone> <key> primary. - Set
default-soa-edit-signed=INCEPTION-INCREMENT(or per-zoneSOA-EDIT). - Keep the primary out of the NS set and the SOA MNAME.
- On Knot, sign the
remotewith the key and allow NOTIFY only from the primary with the key. - Turn on
dnssec-validation: onfor each secondary zone. - Confirm
kdig AXFRwithout the key returnsNOTAUTHfrom a transfer-network host. - Confirm a public host cannot reach the primary at all.
- Alert on DNSSEC validation errors and zone expiry in the Knot logs.
A hidden primary is a network design, not a setting. Get the network right, and the TSIG key only has to protect the transfer, not the whole secret.
H2-CPQE
Learn it on a live range
DNSSEC with Knot, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome