TTR Networks

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

Zones with pdnsutil

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

pdnsutil is the command-line tool for everything stored in PowerDNS's backend: creating zones, adding records, checking and signing them. It's the quickest way to build a zone by hand, and the lab uses it for three zones: the public example.net, the internal internal.example.net, and the reverse zone 0.10.10.in-addr.arpa, for the office LAN 10.10.0.0/24.

Two surprises

The SOA record isn't what you asked for. create-zone example.net ns1.example.net adds the NS record, but the start-of-authority (SOA) record comes from the server's default-soa-content setting, which out of the box is a placeholder: a.misconfigured.dns.server.invalid, serial 0. check-zone doesn't complain, but secondaries use the SOA's serial to know when to transfer the zone. Fix it in two places:

  • Set default-soa-content in the configuration, so every new zone starts with a real SOA (the lab's ns1.conf has it).
  • Replace the SOA of existing zones with a date-based serial (YYYYMMDDnn), and set the zone's SOA-EDIT-API metadata, so changes through the API bump the serial automatically.

The running server doesn't see zones made with pdnsutil straight away. ns1 answered REFUSED for its own brand-new zone, exactly as if it didn't host it. PowerDNS keeps a list of zones in memory and rereads it every zone-cache-refresh-interval (300 seconds by default). A restart picked the zone up immediately. Zones created through the API appear at once.

An authoritative server is not a resolver

Ask ns1 about a name it isn't authoritative for, like www.wikipedia.org, and it answers REFUSED, with dig's warning that recursion was requested but isn't available. That's correct behaviour, and it's exactly why your users need a separate resolver.

The public zone (real output)
root@ns1:~# pdnsutil create-zone example.net ns1.example.net
Creating empty zone 'example.net'
Also adding one NS record

root@ns1:~# pdnsutil add-record example.net @ NS ns2.example.net && pdnsutil add-record example.net ns1 A 203.0.113.53 && pdnsutil add-record example.net ns2 A 203.0.113.54 && pdnsutil add-record example.net www A 203.0.113.80 && pdnsutil add-record example.net mail A 203.0.113.25 && pdnsutil add-record example.net @ MX "10 mail.example.net."
New rrset:
example.net. 3600 IN NS ns1.example.net
example.net. 3600 IN NS ns2.example.net
New rrset:
ns1.example.net. 3600 IN A 203.0.113.53
New rrset:
ns2.example.net. 3600 IN A 203.0.113.54
New rrset:
www.example.net. 3600 IN A 203.0.113.80
New rrset:
mail.example.net. 3600 IN A 203.0.113.25
New rrset:
example.net. 3600 IN MX 10 mail.example.net

root@ns1:~# pdnsutil list-zone example.net
$ORIGIN .
example.net	3600	IN	MX	10 mail.example.net.
example.net	3600	IN	NS	ns1.example.net.
example.net	3600	IN	NS	ns2.example.net.
example.net	3600	IN	SOA	a.misconfigured.dns.server.invalid hostmaster.example.net 0 10800 3600 604800 3600
mail.example.net	3600	IN	A	203.0.113.25
ns1.example.net	3600	IN	A	203.0.113.53
ns2.example.net	3600	IN	A	203.0.113.54
www.example.net	3600	IN	A	203.0.113.80

root@ns1:~# pdnsutil check-zone example.net
Checked 8 records of 'example.net', 0 errors, 0 warnings.
  • Lines 1–3: A zone with one NS record.
  • Lines 5–18: The second nameserver, its address, and the zone's records.
  • Line 25: The placeholder SOA: a nameserver that doesn't exist, and serial 0.
  • Lines 31–32: …and check-zone doesn't mind.
The internal zone and the reverse zone (real output)
root@ns1:~# pdnsutil create-zone internal.example.net ns1.example.net && pdnsutil add-record internal.example.net @ NS ns2.example.net
Creating empty zone 'internal.example.net'
Also adding one NS record
New rrset:
internal.example.net. 3600 IN NS ns1.example.net
internal.example.net. 3600 IN NS ns2.example.net

root@ns1:~# for h in "gw 10.10.0.1" "files 10.10.0.10" "intranet 10.10.0.11" "client 10.10.0.20" "dns 10.10.0.53"; do set -- $h; pdnsutil add-record internal.example.net $1 A $2 >/dev/null; done; pdnsutil create-zone 0.10.10.in-addr.arpa ns1.example.net >/dev/null && pdnsutil add-record 0.10.10.in-addr.arpa @ NS ns2.example.net >/dev/null; for p in "1 gw" "10 files" "11 intranet" "20 client" "53 dns"; do set -- $p; pdnsutil add-record 0.10.10.in-addr.arpa $1 PTR $2.internal.example.net. >/dev/null; done; pdnsutil list-all-zones
Creating empty zone '0.10.10.in-addr.arpa'
example.net
internal.example.net
0.10.10.in-addr.arpa

root@ns1:~# pdnsutil list-zone 0.10.10.in-addr.arpa
$ORIGIN .
0.10.10.in-addr.arpa	3600	IN	NS	ns1.example.net.
0.10.10.in-addr.arpa	3600	IN	NS	ns2.example.net.
0.10.10.in-addr.arpa	3600	IN	SOA	a.misconfigured.dns.server.invalid hostmaster.0.10.10.in-addr.arpa 0 10800 3600 604800 3600
1.0.10.10.in-addr.arpa	3600	IN	PTR	gw.internal.example.net
10.0.10.10.in-addr.arpa	3600	IN	PTR	files.internal.example.net
11.0.10.10.in-addr.arpa	3600	IN	PTR	intranet.internal.example.net
20.0.10.10.in-addr.arpa	3600	IN	PTR	client.internal.example.net
53.0.10.10.in-addr.arpa	3600	IN	PTR	dns.internal.example.net
  • Lines 8–12: Five internal hosts, and a PTR for each in the office's reverse zone.
  • Line 18: The same placeholder SOA.
  • Lines 19–23: Reverse records: 10.10.0.10 is files.internal.example.net.
Fixing the SOA records (real output)
root@ns1:~# for z in example.net internal.example.net 0.10.10.in-addr.arpa; do pdnsutil replace-rrset $z @ SOA 3600 "ns1.example.net. hostmaster.example.net. 2026102101 10800 3600 604800 3600" >/dev/null && pdnsutil set-meta $z SOA-EDIT-API DEFAULT; done
Set 'example.net' meta SOA-EDIT-API = DEFAULT
Set 'internal.example.net' meta SOA-EDIT-API = DEFAULT
Set '0.10.10.in-addr.arpa' meta SOA-EDIT-API = DEFAULT

root@ns1:~# systemctl restart pdns && sleep 1 && dig @203.0.113.53 example.net SOA +short
ns1.example.net. hostmaster.example.net. 2026102101 10800 3600 604800 3600
  • Lines 1–4: A real primary nameserver, a contact, and a date-based serial; plus SOA-EDIT-API so API changes bump the serial.
  • Line 7: The corrected SOA.
A new zone isn't served yet (real output)
root@client:~# dig @203.0.113.53 www.example.net

; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> @203.0.113.53 www.example.net
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 9014
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.example.net.		IN	A

;; Query time: 0 msec
;; SERVER: 203.0.113.53#53(203.0.113.53) (UDP)
;; WHEN: Wed Oct 21 05:39:25 UTC 2026
;; MSG SIZE  rcvd: 44


root@ns1:~# systemctl restart pdns

root@client:~# dig @203.0.113.53 www.example.net +short
203.0.113.80
  • Lines 7–9: REFUSED for a zone ns1 now hosts: the running server hasn't refreshed its list of zones.
  • Lines 22–25: After a restart, it answers.
Asking an authoritative server about someone else's name (real output)
root@client:~# dig @203.0.113.53 www.wikipedia.org

; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> @203.0.113.53 www.wikipedia.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 34152
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.wikipedia.org.		IN	A

;; Query time: 0 msec
;; SERVER: 203.0.113.53#53(203.0.113.53) (UDP)
;; WHEN: Wed Oct 21 05:39:25 UTC 2026
;; MSG SIZE  rcvd: 46
  • Lines 7–9: Refused, and no recursion: ns1 answers only for its own zones. Your users need a resolver.

Check your understanding

After creating a zone with pdnsutil, the server answers REFUSED for it, and pdnsutil list-zone shows the records. What's going on?

  • The running server's list of zones hasn't been refreshed yet; restart, wait for the refresh interval, or create zones through the API.
  • The zone isn't signed.
  • REFUSED means the records are wrong.
  • The client needs recursion turned off.