The recursor
The PowerDNS Recursor is the office's resolver: staff computers send it every DNS question, and it answers from its cache or by asking the right servers, validating DNSSEC as it goes. Its configuration on Debian 13 is YAML, even though the file is still called recursor.conf.
The configuration does four jobs:
- Where to start.
hint_filepoints at the lab root instead of the real root servers, and the trust anchor is the lab root's DS. On a real network you'd leave both at their defaults, which Debian keeps up to date. - Validate DNSSEC.
validation: validatemeans bogus answers become SERVFAIL, even for clients that never asked about DNSSEC. - Internal zones.
forward_zonessends questions aboutinternal.example.netand the office's reverse zone straight to ns1 and ns2, because the public DNS doesn't know about them. - Who may use it.
allow_fromlimits it to the office LAN, so it can't become an open resolver.
Three things went wrong first
- SERVFAIL for everything: No Reachable Authority. The recursor refuses to send queries to private and documentation address ranges (
dont_query), and this lab's "internet" uses documentation addresses. On a real network that default protects you; the lab overrides it, with a comment saying so. - SERVFAIL for internal names: RRSIGs Missing.
internal.example.netsits under the signedexample.net, but isn't delegated from it. So when the recursor gets unsigned answers for it from the forwarders, they look forged. A negative trust anchor (NTA) tells the recursor not to validate below that name. Alternatives are to sign the internal zone and delegate it properly, or to use a name outside your public domain. - Outsiders get silence, not REFUSED. The recursor drops queries from addresses outside
allow_fromwithout replying, so to an outsider it looks like a dead server. Remember that when you're troubleshooting from the wrong network.
With that, the client gets validated answers, with dig's ad (authenticated data) flag, for public names. Internal names and reverse lookups are answered from ns1 and ns2, without ad, because they deliberately aren't validated.
# /etc/powerdns/recursor.conf: PowerDNS Recursor 5 (Debian ships this file in YAML).
dnssec:
# Validate everything, and refuse answers that fail validation.
validation: validate
# The lab's own root key, instead of the real root's (/usr/share/dns/root.key).
trustanchors:
- name: .
dsrecords:
- 49430 13 2 79ec0fb185cf6f5be1df5936655d08fb7dc4e280694535ab8d819da9cccb7757
# internal.example.net isn't delegated from the signed public zone, so its
# (unsigned) answers would look forged. Don't validate below it.
negative_trustanchors:
- name: internal.example.net
reason: internal zone, not delegated from the signed example.net
recursor:
# Where recursion starts: the lab's root server, instead of the real root servers.
hint_file: /etc/powerdns/lab-root.hints
include_dir: /etc/powerdns/recursor.d
security_poll_suffix: ''
# Internal zones aren't in public DNS: ask our own servers for them directly.
forward_zones:
- zone: internal.example.net
forwarders: [203.0.113.53, 203.0.113.54]
- zone: 0.10.10.in-addr.arpa
forwarders: [203.0.113.53, 203.0.113.54]
incoming:
# Only the office may use this resolver.
listen: [10.10.0.53, 127.0.0.1]
allow_from: [10.10.0.0/24, 127.0.0.0/8]
outgoing:
# LAB ONLY: by default the recursor refuses to query private and documentation
# ranges, and this lab's "internet" is 203.0.113.0/24. This is the default list
# without that one range. On a real network, leave dont_query alone.
dont_query: [127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, '::1/128', 'fc00::/7', 'fe80::/10', 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 240.0.0.0/4, '::/96', '::ffff:0:0/96', '100::/64', '2001:db8::/32']- Line 4: Validate DNSSEC for every client, and return SERVFAIL for bogus answers.
- Lines 6–9: The lab root's DS as the trust anchor (a real resolver keeps the real root's).
- Lines 12–14: Don't validate the internal zone (explained below).
- Line 17: Start recursion at the lab root.
- Lines 21–25: Internal names go straight to ns1 and ns2.
- Lines 28–29: Only the office may use this resolver.
- Line 34: Lab only: allow queries to the documentation range this lab uses as its internet.
; /etc/powerdns/lab-root.hints: the lab's root server.
. 3600000 NS ns.root.test.
ns.root.test. 3600000 A 203.0.113.1- Lines 2–3: Where to find the root: the same format as the real root hints file.
root@recursor:~# systemctl restart pdns-recursor && sleep 2 && systemctl status pdns-recursor --no-pager | head -4
● pdns-recursor.service - PowerDNS Recursor
Loaded: loaded (/usr/lib/systemd/system/pdns-recursor.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-10-21 05:45:34 UTC; 2s ago
Invocation: 83f37adb9d63447fb433161134759e40
root@client:~# dig www.example.net
; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> www.example.net
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 4331
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
; EDE: 22 (No Reachable Authority): (delegation .)
;; QUESTION SECTION:
;www.example.net. IN A
;; Query time: 1 msec
;; SERVER: 10.10.0.53#53(10.10.0.53) (UDP)
;; WHEN: Wed Oct 21 05:45:36 UTC 2026
;; MSG SIZE rcvd: 62- Line 12: SERVFAIL…
- Line 17: …because it couldn't reach any server for the root.
root@recursor:~# journalctl -u pdns-recursor --no-pager -o cat | grep -m1 "Will not send queries to" | grep -oE "addresses=\"[^\"]+\""
addresses="127.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 192.168.0.0/16 172.16.0.0/12 ::1/128 fc00::/7 fe80::/10 0.0.0.0/8 192.0.0.0/24 192.0.2.0/24 198.51.100.0/24 203.0.113.0/24 240.0.0.0/4 ::/96 ::ffff:0:0/96 100::/64 2001:db8::/32 0.0.0.0 ::"- Line 2: Private and documentation ranges, including 203.0.113.0/24: the recursor won't send queries there.
root@recursor:~# systemctl restart pdns-recursor && sleep 2 && systemctl is-active pdns-recursor
active
root@client:~# dig www.example.net +dnssec
; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> www.example.net +dnssec
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42363
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 512
;; QUESTION SECTION:
;www.example.net. IN A
;; ANSWER SECTION:
www.example.net. 3600 IN A 203.0.113.80
www.example.net. 3600 IN RRSIG A 13 3 3600 20261029000000 20261008000000 18425 example.net. ZFrSXgN/FmALxwO8W6NL3oNThs7GtgUhKTEu5gzUa4jQJpOMhIx87iZc 9sUP8a24VpoW5aWhSboRgsqOYgtIrQ==
;; Query time: 3 msec
;; SERVER: 10.10.0.53#53(10.10.0.53) (UDP)
;; WHEN: Wed Oct 21 05:46:06 UTC 2026
;; MSG SIZE rcvd: 167- Line 10:
ad: authenticated data. Validated all the way from the lab's root key. - Lines 18–19: The answer and its signature.
root@client:~# dig files.internal.example.net
; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> files.internal.example.net
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 30832
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
; EDE: 10 (RRSIGs Missing)
;; QUESTION SECTION:
;files.internal.example.net. IN A
;; Query time: 0 msec
;; SERVER: 10.10.0.53#53(10.10.0.53) (UDP)
;; WHEN: Wed Oct 21 05:46:15 UTC 2026
;; MSG SIZE rcvd: 61- Line 6: SERVFAIL…
- Line 11: …because, under the signed example.net, these unsigned answers look forged.
root@recursor:~# systemctl restart pdns-recursor && sleep 2 && systemctl is-active pdns-recursor
active
root@client:~# dig files.internal.example.net | grep -E "status|flags|^files"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42081
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 512
files.internal.example.net. 3600 IN A 10.10.0.10
root@client:~# dig www.example.net | grep -E "status|flags|^www"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9948
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 512
www.example.net. 3600 IN A 203.0.113.80
root@client:~# dig -x 10.10.0.53 +short
dns.internal.example.net.- Lines 6–8: Internal names work, without
ad: deliberately not validated. - Line 12: Public names are still validated.
- Lines 16–17: Reverse lookups are answered by ns1 and ns2.
root@outsider:~# dig @10.10.0.53 www.example.net +tries=1 +timeout=3
;; communications error to 10.10.0.53#53: timed out
; <<>> DiG 9.20.29-1~deb13u1-Debian <<>> @10.10.0.53 www.example.net +tries=1 +timeout=3
; (1 server found)
;; global options: +cmd
;; no servers could be reached- Lines 2–7: No reply at all: the recursor silently drops queries from outside allow_from.