Recursor 5.4.0: RPZ matches on '.' root query, breaks DNSSEC validation (Indeterminate instead of Bogus)
Chris Brough via Pdns-users <[email protected]> Sun, 19 Apr 2026 22:48:09 +0100 (BST)
| Newsgroups | gmane.network.dns.powerdns.user |
|---|---|
| Message-ID | <[email protected]> |
--===============1953471144061628904==
Content-Type: multipart/alternative;
boundary="----=_Part_14836_1318503800.1776635289027"
------=_Part_14836_1318503800.1776635289027
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Hi all,
=20
I'm running into behaviour I can't explain and I'd appreciate a second pair=
of eyes before I assume it's a bug. I could easily be doing something obvi=
ous and wrong.
=20
Setup
* PowerDNS Recursor 5.4.0 in Docker (image: powerdns/pdns-recursor-54:l=
atest)
* Host: Debian 13 VM (UmbrelOS - using the Portainer app to manage Dock=
er), Docker host network mode
* Full recursion, no forwarders
* `dnssec.validation: validate`, built-in root trust anchor
* Root zone loaded via `zonetocaches` from Internic (ZONEMD validated S=
ecure at startup)
* 7 RPZ zones loaded via `rpzPrimary` from IPFire DBL (xfr.dbl.ipfire.o=
rg) =E2=80=94 ads, gambling, malware, phishing, piracy, smart-tv, violence.=
Open AXFR, no TSIG.
* `extended_resolution_errors: true`, per-RPZ `extendedErrorCode: 15` a=
nd `extendedErrorExtra` set
Full YAML config and docker-compose.yml available on request.
=20
Symptom
DNSSEC-bogus domains return NOERROR with data instead of SERVFAIL:
$ dig @<host_ip> dnssec-failed.org +dnssec
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18515
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
dnssec-failed.org. 218 IN A 96.99.227.255
dnssec-failed.org. 218 IN RRSIG A 13 2 300 ...
=20
Confirmed validation mode is actually loaded:
$ rec_control get-parameter dnssec
dnssec:
validation: validate
disabled_algorithms:
- '1'
- '3'
- '5'
- '6'
- '7'
- '12'
log_bogus: true
=20
Query log shows `validationState=3D"Indeterminate"` for the query, not Bogu=
s.
=20
Root cause (as far as I can tell)
A rec_control trace-regex 'dnssec-failed.org' trace shows validation tries =
to fetch the root DNSKEY and hits the first RPZ in the config list:
.|DNSKEY:: RPZ Hit; PolicyName=3Dads.rpz.ipfire.org; Trigger=3D.; Hit=
=3D; Type=3DQName; Kind=3DLocal Data
.: Retrieved 0 DNSKeys, state is Indeterminate
org: Updating validation state with cache content for org to Indetermin=
ate
dnssec-failed.org: Updating validation state with cache content for dns=
sec-failed.org to Indeterminate
dnssec-failed.org: Validation status is Indeterminate
=20
Confirming this from a client:
$ dig @<host_ip> . NS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14776
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDE: 15 (Blocked): (Blocked: advertising)
;; QUESTION SECTION:
;. IN NS
=20
Zero-answer NOERROR on `. NS`, tagged by the first RPZ. This is not specifi=
c to the ads list =E2=80=94 I tested by removing ads.rpz.ipfire.org from co=
nfig, and whichever RPZ moved into first position then shows the same behav=
iour on root queries. So every IPFire RPZ I'm loading exhibits this, which =
makes me think it's something about the way they're structured or the way P=
DNS loads/evaluates them, rather than a specific zone being poisoned.
=20
What dump-rpz shows:
$ rec_control dump-rpz ads.rpz.ipfire.org /tmp/ads.txt
$ head -5 /tmp/ads.txt
ads.rpz.ipfire.org. IN SOA primary.dbl.ipfire.org. hostmaster.ipfire.or=
g. 1776630606 3600 600 3600000 60
stbg.stanbicbank.co.zw.ads.rpz.ipfire.org. 60 IN CNAME .
stats.zpl.zone.ads.rpz.ipfire.org. 60 IN CNAME .
*.a.userscript.zone.ads.rpz.ipfire.org. 60 IN CNAME .
a.userscript.zone.ads.rpz.ipfire.org. 60 IN CNAME .
=20
Apex has only SOA (NS stripped). Nothing in the dumped zone should produce =
a `.` QName trigger as far as I can read it =E2=80=94 but the trace clearly=
shows one matching, with `Hit=3D` empty.
=20
The IPFire source zone at xfr.dbl.ipfire.org contains at its apex: SOA, NS,=
and an `_info IN TXT` record one label below the apex. Nothing else unusua=
l that I can see.
=20
RPZ config (abbreviated)
recursor:
rpzs:
- name: ads.rpz.ipfire.org
addresses: [ 'xfr.dbl.ipfire.org:53' ]
policyName: ads.rpz.ipfire.org
extendedErrorCode: 15
extendedErrorExtra: 'Blocked: advertising'
- name: gambling.rpz.ipfire.org
addresses: [ 'xfr.dbl.ipfire.org:53' ]
policyName: gambling.rpz.ipfire.org
extendedErrorCode: 15
extendedErrorExtra: 'Blocked: gambling'
# ... 5 more in same pattern
system_resolver_ttl: 300
extended_resolution_errors: true
=20
No `defpol` is set on any zone.
=20
Am I configuring these RPZs wrong in a way that causes `.` to match as a QN=
ame trigger? I can't find anything in docs or the RFC that would point ther=
e.
=20
Happy to provide the full YAML, full trace log, full zone dump, or anything=
else useful. I didn't attach them here to keep the email readable.
=20
Thanks,
Chris
------=_Part_14836_1318503800.1776635289027
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<!doctype html>
<html>
<head>
<meta charset=3D"UTF-8">
</head>
<body>
<div class=3D"default-style" style=3D"">
<div class=3D"default-style" style=3D"">Hi all,</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">I'm running into behaviour I can=
't explain and I'd appreciate a second pair of eyes before I assume it's a =
bug. I could easily be doing something obvious and wrong.</div>
<div class=3D"default-style" style=3D""> </div>
<span style=3D"text-decoration: underline;"><strong>Setup</strong></span=
>
<div class=3D"default-style" style=3D"">
<ul>
<li>PowerDNS Recursor 5.4.0 in Docker (image: powerdns/pdns-recursor-5=
4:latest)</li>
<li>Host: Debian 13 VM (UmbrelOS - using the Portainer app to manage D=
ocker), Docker host network mode</li>
<li>Full recursion, no forwarders</li>
<li>`dnssec.validation: validate`, built-in root trust anchor</li>
<li>Root zone loaded via `zonetocaches` from Internic (ZONEMD validate=
d Secure at startup)</li>
<li>7 RPZ zones loaded via `rpzPrimary` from IPFire DBL (xfr.dbl.ipfir=
e.org) =E2=80=94 ads, gambling, malware, phishing, piracy, smart-tv, violen=
ce. Open AXFR, no TSIG.</li>
<li>`extended_resolution_errors: true`, per-RPZ `extendedErrorCode: 15=
` and `extendedErrorExtra` set</li>
</ul>
Full YAML config and docker-compose.yml available on request.
</div>
<div class=3D"default-style" style=3D""> </div>
<strong><span style=3D"text-decoration: underline;">Symptom</span></stro=
ng>
<div class=3D"default-style" style=3D"">DNSSEC-bogus domains return NOER=
ROR with data instead of SERVFAIL:</div>
<div class=3D"default-style" style=3D"">
<code> $ dig @<host_ip> dnssec-failed.org +dnssec</c=
ode>
<br>
<code> ;; ->>HEADER<<- opcode: QUERY, status: =
NOERROR, id: 18515</code>
<br>
<code> ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY:=
0, ADDITIONAL: 1</code>
<br>
<code> ;; ANSWER SECTION:</code>
<br>
<code> dnssec-failed.org. 218 IN A  =
;96.99.227.255</code>
<br>
<code> dnssec-failed.org. 218 IN RRSIG &=
nbsp;A 13 2 300 ...</code>
</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">Confirmed validation mode is act=
ually loaded:</div>
<div class=3D"default-style" style=3D"">
<code> $ rec_control get-parameter dnssec</code>
<br>
<code> dnssec:</code>
<br>
<code> validation: validate</code>
<br>
<code> disabled_algorithms:</code>
<br>
<code> - '1'</code>
<br>
<code> - '3'</code>
<br>
<code> - '5'</code>
<br>
<code> - '6'</code>
<br>
<code> - '7'</code>
<br>
<code> - '12'</code>
<br>
<code> log_bogus: true</code>
</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">Query log shows `validationState=
=3D"Indeterminate"` for the query, not Bogus.</div>
<div class=3D"default-style" style=3D""> </div>
<strong><span style=3D"text-decoration: underline;">Root cause (as far a=
s I can tell)</span></strong>
</div>
<div class=3D"default-style" style=3D"">
<div class=3D"default-style" style=3D"">A rec_control trace-regex 'dnsse=
c-failed.org' trace shows validation tries to fetch the root DNSKEY and hit=
s the first RPZ in the config list:</div>
<div class=3D"default-style" style=3D"">
<code> .|DNSKEY:: RPZ Hit; PolicyName=3Dads.rpz.ipfire.org=
; Trigger=3D.; Hit=3D; Type=3DQName; Kind=3DLocal Data</code>
<br>
<code> .: Retrieved 0 DNSKeys, state is Indeterminate</cod=
e>
<br>
<code> org: Updating validation state with cache content f=
or org to Indeterminate</code>
<br>
<code> dnssec-failed.org: Updating validation state with c=
ache content for dnssec-failed.org to Indeterminate</code>
<br>
<code> dnssec-failed.org: Validation status is Indetermina=
te</code>
</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">Confirming this from a client:</=
div>
<div class=3D"default-style" style=3D"">
<code> $ dig @<host_ip> . NS</code>
<br>
<code> ;; ->>HEADER<<- opcode: QUERY, status: =
NOERROR, id: 14776</code>
<br>
<code> ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY:=
0, ADDITIONAL: 1</code>
<br>
<code> ; EDE: 15 (Blocked): (Blocked: advertising)</code>
<br>
<code> ;; QUESTION SECTION:</code>
<br>
<code> ;. IN NS</code>
</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">Zero-answer NOERROR on `. NS`, t=
agged by the first RPZ. This is not specific to the ads list =E2=80=94 I te=
sted by removing ads.rpz.ipfire.org from config, and whichever RPZ moved in=
to first position then shows the same behaviour on root queries. So every I=
PFire RPZ I'm loading exhibits this, which makes me think it's something ab=
out the way they're structured or the way PDNS loads/evaluates them, rather=
than a specific zone being poisoned.</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">What dump-rpz shows:</div>
<div class=3D"default-style" style=3D"">
<code> $ rec_control dump-rpz ads.rpz.ipfire.org /tmp/ads.=
txt</code>
<br>
<code> $ head -5 /tmp/ads.txt</code>
<br>
<code> ads.rpz.ipfire.org. IN SOA primary.dbl.ipfire.org. =
hostmaster.ipfire.org. 1776630606 3600 600 3600000 60</code>
<br>
<code> stbg.stanbicbank.co.zw.ads.rpz.ipfire.org. 60 IN CN=
AME .</code>
<br>
<code> stats.zpl.zone.ads.rpz.ipfire.org. 60 IN CNAME .</c=
ode>
<br>
<code> *.a.userscript.zone.ads.rpz.ipfire.org. 60 IN CNAME=
.</code>
<br>
<code> a.userscript.zone.ads.rpz.ipfire.org. 60 IN CNAME .=
</code>
</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">Apex has only SOA (NS stripped).=
Nothing in the dumped zone should produce a `.` QName trigger as far as I =
can read it =E2=80=94 but the trace clearly shows one matching, with `Hit=
=3D` empty.</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">The IPFire source zone at xfr.db=
l.ipfire.org contains at its apex: SOA, NS, and an `_info IN TXT` record on=
e label below the apex. Nothing else unusual that I can see.</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">
<span style=3D"text-decoration: underline;"><strong>RPZ config (abbrevi=
ated)</strong></span>
</div>
<div class=3D"default-style" style=3D"">
<code> recursor:</code>
<br>
<code> rpzs:</code>
<br>
<code> - name: ads.rpz.ipfire.org</code>
<br>
<code> addresses: [ 'xfr.dbl.ipfire.o=
rg:53' ]</code>
<br>
<code> policyName: ads.rpz.ipfire.org=
</code>
<br>
<code> extendedErrorCode: 15</code>
<br>
<code> extendedErrorExtra: 'Blocked: =
advertising'</code>
<br>
<code> - name: gambling.rpz.ipfire.org</code=
>
<br>
<code> addresses: [ 'xfr.dbl.ipfire.o=
rg:53' ]</code>
<br>
<code> policyName: gambling.rpz.ipfir=
e.org</code>
<br>
<code> extendedErrorCode: 15</code>
<br>
<code> extendedErrorExtra: 'Blocked: =
gambling'</code>
<br>
<code> # ... 5 more in same pattern</code>
<br>
<code> system_resolver_ttl: 300</code>
<br>
<code> extended_resolution_errors: true</code>
</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">No `defpol` is set on any zone.<=
/div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">
<div class=3D"default-style" style=3D"">
<div class=3D"default-style" style=3D"">Am I configuring these RPZs wr=
ong in a way that causes `.` to match as a QName trigger? I can't find anyt=
hing in docs or the RFC that would point there.</div>
</div>
<div class=3D"default-style" style=3D"font-family: -apple-system, Blink=
MacSystemFont, helvetica, sans-serif;"> </div>
</div>
<div class=3D"default-style" style=3D"">Happy to provide the full YAML, =
full trace log, full zone dump, or anything else useful. I didn't attach th=
em here to keep the email readable.</div>
<div class=3D"default-style" style=3D""> </div>
<div class=3D"default-style" style=3D"">
Thanks,
<br>
Chris
</div>
</div>
</body>
</html>
------=_Part_14836_1318503800.1776635289027--
--===============1953471144061628904==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
_______________________________________________
Pdns-users mailing list
[email protected]
https://mailman.powerdns.com/mailman/listinfo/pdns-users
--===============1953471144061628904==--