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"">&nbsp;</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"">&nbsp;</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"">&nbsp;</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>&nbsp; &nbsp; $ dig @&lt;host_ip&gt; dnssec-failed.org +dnssec</c=
ode>
    <br>
    <code>&nbsp; &nbsp; ;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: =
NOERROR, id: 18515</code>
    <br>
    <code>&nbsp; &nbsp; ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY:=
 0, ADDITIONAL: 1</code>
    <br>
    <code>&nbsp; &nbsp; ;; ANSWER SECTION:</code>
    <br>
    <code>&nbsp; &nbsp; dnssec-failed.org. &nbsp;218 &nbsp;IN &nbsp;A &nbsp=
;96.99.227.255</code>
    <br>
    <code>&nbsp; &nbsp; dnssec-failed.org. &nbsp;218 &nbsp;IN &nbsp;RRSIG &=
nbsp;A 13 2 300 ...</code>
   </div>
   <div class=3D"default-style" style=3D"">&nbsp;</div>
   <div class=3D"default-style" style=3D"">Confirmed validation mode is act=
ually loaded:</div>
   <div class=3D"default-style" style=3D"">
    <code>&nbsp; &nbsp; $ rec_control get-parameter dnssec</code>
    <br>
    <code>&nbsp; &nbsp; dnssec:</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; validation: validate</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; disabled_algorithms:</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; - '1'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; - '3'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; - '5'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; - '6'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; - '7'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; - '12'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; log_bogus: true</code>
   </div>
   <div class=3D"default-style" style=3D"">&nbsp;</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"">&nbsp;</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>&nbsp; &nbsp; .|DNSKEY:: RPZ Hit; PolicyName=3Dads.rpz.ipfire.org=
; Trigger=3D.; Hit=3D; Type=3DQName; Kind=3DLocal Data</code>
    <br>
    <code>&nbsp; &nbsp; .: Retrieved 0 DNSKeys, state is Indeterminate</cod=
e>
    <br>
    <code>&nbsp; &nbsp; org: Updating validation state with cache content f=
or org to Indeterminate</code>
    <br>
    <code>&nbsp; &nbsp; dnssec-failed.org: Updating validation state with c=
ache content for dnssec-failed.org to Indeterminate</code>
    <br>
    <code>&nbsp; &nbsp; dnssec-failed.org: Validation status is Indetermina=
te</code>
   </div>
   <div class=3D"default-style" style=3D"">&nbsp;</div>
   <div class=3D"default-style" style=3D"">Confirming this from a client:</=
div>
   <div class=3D"default-style" style=3D"">
    <code>&nbsp; &nbsp; $ dig @&lt;host_ip&gt; . NS</code>
    <br>
    <code>&nbsp; &nbsp; ;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: =
NOERROR, id: 14776</code>
    <br>
    <code>&nbsp; &nbsp; ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY:=
 0, ADDITIONAL: 1</code>
    <br>
    <code>&nbsp; &nbsp; ; EDE: 15 (Blocked): (Blocked: advertising)</code>
    <br>
    <code>&nbsp; &nbsp; ;; QUESTION SECTION:</code>
    <br>
    <code>&nbsp; &nbsp; ;. &nbsp; &nbsp;IN &nbsp; &nbsp;NS</code>
   </div>
   <div class=3D"default-style" style=3D"">&nbsp;</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"">&nbsp;</div>
   <div class=3D"default-style" style=3D"">What dump-rpz shows:</div>
   <div class=3D"default-style" style=3D"">
    <code>&nbsp; &nbsp; $ rec_control dump-rpz ads.rpz.ipfire.org /tmp/ads.=
txt</code>
    <br>
    <code>&nbsp; &nbsp; $ head -5 /tmp/ads.txt</code>
    <br>
    <code>&nbsp; &nbsp; ads.rpz.ipfire.org. IN SOA primary.dbl.ipfire.org. =
hostmaster.ipfire.org. 1776630606 3600 600 3600000 60</code>
    <br>
    <code>&nbsp; &nbsp; stbg.stanbicbank.co.zw.ads.rpz.ipfire.org. 60 IN CN=
AME .</code>
    <br>
    <code>&nbsp; &nbsp; stats.zpl.zone.ads.rpz.ipfire.org. 60 IN CNAME .</c=
ode>
    <br>
    <code>&nbsp; &nbsp; *.a.userscript.zone.ads.rpz.ipfire.org. 60 IN CNAME=
 .</code>
    <br>
    <code>&nbsp; &nbsp; a.userscript.zone.ads.rpz.ipfire.org. 60 IN CNAME .=
</code>
   </div>
   <div class=3D"default-style" style=3D"">&nbsp;</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"">&nbsp;</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"">&nbsp;</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>&nbsp; &nbsp; recursor:</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; rpzs:</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; - name: ads.rpz.ipfire.org</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; addresses: [ 'xfr.dbl.ipfire.o=
rg:53' ]</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; policyName: ads.rpz.ipfire.org=
</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; extendedErrorCode: 15</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; extendedErrorExtra: 'Blocked: =
advertising'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; - name: gambling.rpz.ipfire.org</code=
>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; addresses: [ 'xfr.dbl.ipfire.o=
rg:53' ]</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; policyName: gambling.rpz.ipfir=
e.org</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; extendedErrorCode: 15</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; extendedErrorExtra: 'Blocked: =
gambling'</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; &nbsp; # ... 5 more in same pattern</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; system_resolver_ttl: 300</code>
    <br>
    <code>&nbsp; &nbsp; &nbsp; extended_resolution_errors: true</code>
   </div>
   <div class=3D"default-style" style=3D"">&nbsp;</div>
   <div class=3D"default-style" style=3D"">No `defpol` is set on any zone.<=
/div>
   <div class=3D"default-style" style=3D"">&nbsp;</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;">&nbsp;</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"">&nbsp;</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==--