Zones with pdnsutil
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-contentin the configuration, so every new zone starts with a real SOA (the lab'sns1.confhas it). - Replace the SOA of existing zones with a date-based serial (
YYYYMMDDnn), and set the zone'sSOA-EDIT-APImetadata, 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.
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-zonedoesn't mind.
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.
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-APIso API changes bump the serial. - Line 7: The corrected SOA.
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.
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.