Skip to content

fix(roles/bind)!: validate DNSSEC and follow the crypto policy (fix #355, fix #356) - #385

Open
NavidSassan wants to merge 3 commits into
mainfrom
fix/bind-dnssec
Open

NavidSassan wants to merge 3 commits into
mainfrom
fix/bind-dnssec

Conversation

@NavidSassan

@NavidSassan NavidSassan commented Sep 15, 2026

Copy link
Copy Markdown
Member

What

The bind role validates DNSSEC and follows the system-wide crypto policy, like the named.conf the bind package ships on RHEL 8, 9 and 10. Breaking, see below.

  • bind (fix bind: DNSSEC validation is hardcoded off #355): dnssec-validation yes by default, switchable with bind__dnssec_validation. Answers for zones that named forwards to or stubs from internal servers fail validation below a signed parent (.internal, or a subdomain of a signed public domain), so the forward, stub and static-stub zones in bind__zones go into validate-except automatically; other zones can be listed in bind__dnssec_validate_except. BIND 9.11 on RHEL 8 has no validate-except, so the role stops there with a clear message if anything needs excluding.
  • bind (fix bind: named does not follow the system-wide crypto policy #356): the options block keeps the packaged include "/etc/crypto-policies/back-ends/bind.config", plus secroots-file, recursing-file, geoip-directory (RHEL 9/10) and dnssec-enable yes (RHEL 8). The template now follows the packaged file's statements and order, with the Linuxfabrik options (ACLs, forward only, forwarders, RPZ, version) grouped at the end. allow-query { localhost; } and the loopback-only listen-on are deliberately not taken over.
  • bind: drops disable-empty-zone yes;, which takes a zone name, not a boolean, and had no effect.
  • bind: reverse lookups for private and special-use addresses (RFC 1918, RFC 6303) are answered locally from BIND's automatic empty zones. The global forward only kept named from creating any of them, so these lookups went to the forwarders. The template switches forwarding off for exactly these zones (type forward; forwarders { };), except for zones that bind__zones defines and parents of forward / stub zones in bind__zones.

Test

No Molecule scenario exists for bind; verified with the bind playbook against Rocky 8, 9 and 10 ubi-init containers, each with a primary zone and a second named on port 5300 serving corp.internal and corp.isc.org as forward zones:

  • Rocky 9 and 10: local zone NOERROR, forward zones NOERROR, dnssec-failed.org SERVFAIL, isc.org with the ad flag
  • Rocky 10 and Rocky 8 with bind__dnssec_validation: false: forward zones and dnssec-failed.org resolve
  • Rocky 8 without forward zones: validation works; with forward zones and validation on, the role stops at the assert
  • named-checkconf -p shows the policy's disable-algorithms, rndc secroots / rndc recursing write to /var/named/data/, second run changed=0
  • the disable-empty-zone removal: identical automatic empty zones before and after on BIND 9.11, 9.16 and 9.18, with a control that drops one zone
  • empty zones, against a logging forwarder on Rocky 8, 9 and 10: private reverse lookups answered locally, master zones in bind__zones answered from their files, a forward zone below 172.16.0.0/16 and its parent still forwarded, public lookups still forwarded and never resolved iteratively

Not covered: a real RHEL host with SELinux enforcing.

Breaking Changes

named validates DNSSEC, so forged or broken answers for signed zones are answered with SERVFAIL. Internal zones below a signed domain or TLD that named does not get from a forward or stub zone in bind__zones (for example via bind__named_conf_raw) have to be listed in bind__dnssec_validate_except, or set bind__dnssec_validation: false. On RHEL 8 with forward or stub zones, set bind__dnssec_validation: false.

`disable-empty-zone` takes the name of a built-in empty zone to disable
(cfg_type_astring in lib/isccfg/namedconf.c), not a boolean, so
`disable-empty-zone yes;` disabled a zone named "yes.", which does not
exist. The boolean switch would be `empty-zones-enable`.

Verified on Rocky 8, 9 and 10 (bind 9.11.36, 9.16.23, 9.18.33) with the
template rendered before and after: with the role's `forward only;`
named creates no automatic empty zones either way; without it, both
render the same 97/97/98 empty zones and answer `dig -x 10.1.2.3` from
10.IN-ADDR.ARPA, while the control `disable-empty-zone
"10.IN-ADDR.ARPA";` drops exactly that zone.
, fix #356)

The template hardcoded `dnssec-validation no;` and dropped the
crypto-policies include that the named.conf of the bind package carries
on RHEL 8, 9 and 10. Rebuild the options block after the packaged file:
same statements in the same order (secroots-file, recursing-file,
geoip-directory on EL 9+, dnssec-enable yes on EL 8, the include), with
the Linuxfabrik options (ACLs, forward only, forwarders, RPZ, version)
grouped at the end. allow-query { localhost; } and the loopback-only
listen-on are deliberately not taken over.

bind__dnssec_validation (default true) renders dnssec-validation.
Answers for zones forwarded to or stubbed from internal servers fail
validation below a signed parent: host.corp.internal and
host.corp.isc.org forwarded to a local named return SERVFAIL on bind
9.11.36, 9.16.23 and 9.18.33. The forward, stub and static-stub zones
of bind__zones and the new bind__dnssec_validate_except therefore go
into validate-except. BIND 9.11 rejects validate-except (unknown
option), so on RHEL 8 the role asserts that nothing needs excluding.

The crypto policy's disable-algorithms/disable-ds-digests only feed the
validator and trust anchor loading (bin/named/server.c,
lib/dns/validator.c, lib/ns/query.c in 9.18.33), so the include takes
effect together with validation.

Verified with the bind playbook on Rocky 8, 9 and 10 ubi-init
containers: named active, local zone NOERROR, forwarded zones NOERROR,
dnssec-failed.org SERVFAIL, isc.org with the ad flag, both answered
without validation when bind__dnssec_validation is false; the EL 8 host
with forward zones stops at the assert; rndc secroots/recursing write
to /var/named/data; second run changed=0.
BIND does not create an automatic empty zone (RFC 6303) for a name it
would forward with policy `only` (bin/named/server.c in 9.18.33), and
the global `forward only` plus forwarders covers every name. named
therefore created none of its 97/98 empty zones, and reverse lookups
for private and special-use addresses went to the forwarders: a logging
forwarder received 5.2.42.10.in-addr.arpa, 7.7.168.192.in-addr.arpa and
1.0.16.172.in-addr.arpa on bind 9.11.36, 9.16.23 and 9.18.33, also with
a 1.42.10.in-addr.arpa zone in bind__zones. `empty-zones-enable yes`
does not change that; `forward first` does, but falls back to iterative
resolution when the forwarders fail.

Render a `type forward; forwarders { };` zone for each name of BIND's
empty_zones[] list, which switches forwarding off for that name only and
lets named create the empty zone. resolver.arpa is only in the 9.18
list, 0.in-addr.arpa and the ::1 reverse zone are defined by
named.rfc1912.zones. Skipped are names that bind__zones defines (a
duplicate zone is an error) and the parents of forward and stub zones
in bind__zones, since an empty zone answered 10.99.0.1 with NXDOMAIN
instead of passing it to the forwarders of a 99.10.in-addr.arpa zone.

Verified on Rocky 8, 9 and 10 against a logging forwarder: private
reverse lookups (10/8, 172.17/16, 100.64/10, 169.254/16, fd00::/8,
fe80::/10) answered NXDOMAIN from the empty zones, master zones in
bind__zones answered from their files, a forward zone and its parent
still forwarded, public PTR and A lookups still forwarded, never
resolved iteratively. The bind playbook answers `dig -x 10.1.2.3` from
10.IN-ADDR.ARPA and the second run reports changed=0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bind: named does not follow the system-wide crypto policy bind: DNSSEC validation is hardcoded off

1 participant