TTR Networks

Intermediate · PowerDNS on Debian: authoritative, secondary, DNSSEC and recursor · section 5 of 10

A secondary server

✓ Lab-tested 21 October 2026 on Debian 13.7, PowerDNS Authoritative 4.9.17, PowerDNS Recursor 5.2.13, BIND dig 9.20.29

A second authoritative server isn't optional: if your only server is down, your domain disappears from the internet. The classic arrangement is a secondary that copies zones from the primary using standard DNS mechanisms, so the two can even run different software.

  • The primary sends a NOTIFY message when a zone changes.
  • The secondary asks for the primary's SOA, sees a higher serial, and requests the zone with an AXFR (a full zone transfer over TCP).
  • If a NOTIFY is lost, the secondary still checks the serial on its own timer (the SOA's refresh value).

On ns2, pdnsutil create-secondary-zone names each zone and its primary. The first attempt is refused: ns2 reports Not Authorized, and ns1's log says exactly why: denied: client IP has no permission. A primary only lets listed addresses transfer zones (allow-axfr-ips), and nobody is listed until you add them. Without that rule, anyone could download your whole zone and see every name in it. ns2 also backs off after a failure; pdns_control retrieve asks it to try again now.

How fast is NOTIFY?

After a change through the API, ns2 had the new record 46 seconds later. PowerDNS doesn't send NOTIFY the instant a zone changes: it checks for changed serials every xfr-cycle-interval (60 seconds by default), and notifies then. For changes that must go out at once, pdns_control notify example.net queues it immediately; in the lab, ns2 had the change within two seconds.

/etc/powerdns/pdns.d/ns2.conf
# /etc/powerdns/pdns.d/ns2.conf: the secondary authoritative server.
launch=gsqlite3
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
gsqlite3-dnssec=yes
local-address=203.0.113.54

# Fetch zones from their primary, and accept its NOTIFY messages.
secondary=yes
allow-notify-from=203.0.113.53
  • Line 4: Needed so the secondary can store signed zones later.
  • Lines 8–9: A secondary: transfer zones from primaries, and accept NOTIFY from ns1.
The first transfer is refused (real output)
root@ns2:~# for z in example.net internal.example.net 0.10.10.in-addr.arpa; do pdnsutil create-secondary-zone $z 203.0.113.53; done
Creating secondary zone 'example.net', with primaries '203.0.113.53'
Creating secondary zone 'internal.example.net', with primaries '203.0.113.53'
Creating secondary zone '0.10.10.in-addr.arpa', with primaries '203.0.113.53'

root@ns2:~# systemctl restart pdns && sleep 8 && journalctl -u pdns --no-pager -o cat --since "-10s" | grep -iE "axfr|refused|transfer|example" | head -6
XFR-in zone: 'example.net', primary: '203.0.113.53', unable to xfr zone (ResolverException): AXFR chunk error: Server Not Authoritative for zone / Not Authorized (This was attempt number 1. Excluding zone from secondary-checks until 1792561320)
XFR-in zone: 'internal.example.net', primary: '203.0.113.53', unable to xfr zone (ResolverException): AXFR chunk error: Server Not Authoritative for zone / Not Authorized (This was attempt number 1. Excluding zone from secondary-checks until 1792561320)
XFR-in zone: '0.10.10.in-addr.arpa', primary: '203.0.113.53', unable to xfr zone (ResolverException): AXFR chunk error: Server Not Authoritative for zone / Not Authorized (This was attempt number 1. Excluding zone from secondary-checks until 1792561320)

root@ns1:~# journalctl -u pdns --no-pager -o cat --since "-15s" | grep -iE "axfr|refused|denied" | head -3
AXFR-out zone 'example.net', client '203.0.113.54:51467', transfer initiated
AXFR-out zone 'internal.example.net', client '203.0.113.54:41545', transfer initiated
AXFR-out zone 'example.net', client '203.0.113.54:51467', denied: client IP has no permission
  • Lines 1–4: Three secondary zones, all from ns1.
  • Lines 7–9: ns2: Not Authorized, and it backs off before trying again.
  • Line 14: ns1 says why: ns2 isn't allowed to transfer zones.
Allowed, and transferred (real output)
root@ns1:~# grep allow-axfr-ips /etc/powerdns/pdns.d/ns1.conf && systemctl restart pdns
allow-axfr-ips=203.0.113.54

root@ns2:~# for z in example.net internal.example.net 0.10.10.in-addr.arpa; do pdns_control retrieve $z; done
Added retrieval request for 'example.net' from primary 203.0.113.53
Added retrieval request for 'internal.example.net' from primary 203.0.113.53
Added retrieval request for '0.10.10.in-addr.arpa' from primary 203.0.113.53

root@ns2:~# journalctl -u pdns --no-pager -o cat | grep "committed" | head -3
AXFR-in zone: 'example.net', primary: '203.0.113.53', zone committed with serial 2026102101
AXFR-in zone: 'internal.example.net', primary: '203.0.113.53', zone committed with serial 2026102102
AXFR-in zone: '0.10.10.in-addr.arpa', primary: '203.0.113.53', zone committed with serial 2026102101

root@client:~# for s in 203.0.113.53 203.0.113.54; do echo "$s: $(dig @$s internal.example.net SOA +short)"; done; dig @203.0.113.54 printer.internal.example.net +short
203.0.113.53: ns1.example.net. hostmaster.example.net. 2026102102 10800 3600 604800 3600
203.0.113.54: ns1.example.net. hostmaster.example.net. 2026102102 10800 3600 604800 3600
10.10.0.30
  • Lines 1–2: ns2's address in allow-axfr-ips.
  • Lines 4–7: Ask ns2 to try again now instead of waiting out its back-off.
  • Lines 10–12: All three zones copied.
  • Lines 15–17: Both servers serve the same serial, including the record added through the API.
How quickly changes reach the secondary (real output)
mac$ ./notify-test.sh
ns2 served docs.example.net 46 seconds after the change

root@ns1:~# curl -s -X PATCH -H "X-API-Key: lab-only-api-key" -H 'Content-Type: application/json' http://127.0.0.1:8081/api/v1/servers/localhost/zones/example.net. -d '{"rrsets":[{"name":"status.example.net.","type":"A","ttl":3600,"changetype":"REPLACE","records":[{"content":"203.0.113.83","disabled":false}]}]}' -w 'HTTP %{http_code}\n' && pdns_control notify example.net
HTTP 204
Added to queue

root@client:~# sleep 2; dig @203.0.113.54 status.example.net +short; dig @203.0.113.54 example.net SOA +short
203.0.113.83
ns1.example.net. hostmaster.example.net. 2026102104 10800 3600 604800 3600

root@ns2:~# journalctl -u pdns --no-pager -o cat --since "-10s" | cut -c1-120
AXFR-in zone: 'example.net', primary: '203.0.113.53', zone committed with serial 2026102104
  • Lines 1–2: With nothing but PowerDNS's own timer: 46 seconds.
  • Lines 4–6: Change a record, then send NOTIFY now.
  • Lines 8–10: ns2 has it within two seconds.
  • Lines 12–13: The transfer, from ns2's side.
notify-test.sh (the timing script, run from the lab directory)
#!/bin/sh
# Change a record on ns1 through the API, then time how long ns2 takes to serve it.
cd "$(dirname "$0")"
x() { docker compose -f lab-compose.yaml exec -T "$1" sh -c "$2"; }
start=$(date +%s)
x ns1 "curl -s -X PATCH -H 'X-API-Key: lab-only-api-key' -H 'Content-Type: application/json' http://127.0.0.1:8081/api/v1/servers/localhost/zones/example.net. -d '{\"rrsets\":[{\"name\":\"docs.example.net.\",\"type\":\"A\",\"ttl\":3600,\"changetype\":\"REPLACE\",\"records\":[{\"content\":\"203.0.113.82\",\"disabled\":false}]}]}'"
until [ -n "$(x client 'dig @203.0.113.54 docs.example.net +short')" ]; do sleep 1; done
echo "ns2 served docs.example.net $(( $(date +%s) - start )) seconds after the change"

Check your understanding

You add a record on the primary through the API, but the secondary still serves the old serial ten minutes later. Which of these would NOT cause that?

  • The zone is signed with DNSSEC.
  • The zone's kind is Native, so the primary never sends NOTIFY.
  • The zone has no SOA-EDIT-API, so the serial didn't change.
  • The secondary isn't in the primary's allow-axfr-ips.