DNS and DNSSEC

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.

Updated Houssam Hammoudi, CTOTested with PowerDNS Authoritative 5.1.4, Knot DNS 3.5.4, BIND delv 9.20.29

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

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.

text
            transfer network (private)            public
PowerDNS primary 203.0.113.10  <---->  Knot 203.0.113.20 | 198.51.100.20  <---- resolvers

PowerDNS, /etc/powerdns/pdns.d/primary.conf:

ini
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=no

Zone setup on the primary:

bash
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 registrar

The NS set lists only public secondaries. The primary never appears in it:

text
example.com.  NS  ns1.example.com.   ; Knot, 198.51.100.20
example.com.  NS  ns2.example.net.   ; Knot, another provider or region

Knot, /etc/knot/knot.conf on each secondary:

yaml
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 anyone

Firewall on the primary host, in addition to the network design:

bash
# 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 drop

Prove 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:

bash
pdnsutil zone show example.com
text
This 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:

text
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 9

A public client gets the NS set without the primary, and a validated answer from the secondary:

bash
kdig @198.51.100.20 example.com NS +norecurse +short
delv @198.51.100.20 -a anchor.conf +root=example.com www.example.com A
text
ns1.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:

bash
kdig @203.0.113.10 example.com SOA +norecurse +timeout=2 +retry=0
text
;; 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:

bash
kdig @203.0.113.10 example.com AXFR
text
;; 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:

bash
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
text
;; 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 initiated

This is why the network, not the ACL, keeps the primary hidden.

An unsigned NOTIFY to the secondary is refused:

bash
dig @198.51.100.20 example.com SOA +opcode=notify +norecurse
text
;; ->>HEADER<<- opcode: NOTIFY, status: NOTAUTH, id: 38374

A change on the primary reaches the secondary by signed NOTIFY and IXFR:

bash
pdnsutil rrset add example.com api.example.com A 300 192.0.2.20
pdnsutil zone increase-serial example.com
pdns_control notify example.com
text
Added 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 seconds
bash
kdig @198.51.100.20 api.example.com A +dnssec +norecurse +noall +answer
text
api.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= and only-notify= to empty; set also-notify to the secondaries.
  • Create one hmac-sha256 TSIG key for transfers and activate it with pdnsutil tsigkey activate <zone> <key> primary.
  • Set default-soa-edit-signed=INCEPTION-INCREMENT (or per-zone SOA-EDIT).
  • Keep the primary out of the NS set and the SOA MNAME.
  • On Knot, sign the remote with the key and allow NOTIFY only from the primary with the key.
  • Turn on dnssec-validation: on for each secondary zone.
  • Confirm kdig AXFR without the key returns NOTAUTH from 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 free

The 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