Skip to content

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

Merged
NavidSassan merged 11 commits into
mainfrom
fix/bind-dnssec
Oct 2, 2026
Merged

NavidSassan merged 11 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. Along the way the role gets a Molecule scenario, which found a regression for slave zones.

  • 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. The check runs in its own validation block tagged always.
  • 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 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 / listen-on-v6 are deliberately not taken over; the README documents this under "How the Role Behaves" and as "Deviates from the upstream default" on bind__listen_on_addresses and bind__listen_ipv6.
  • 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, static-stub and stub zones in bind__zones. Without recursion named creates no empty zones at all, so the blocks are only rendered with bind__recursion: true.
  • bind: zones with type: 'slave' get their file statement again. b9a9df6 left slave out of the list of types that take one, so such a secondary kept the transferred zone in memory only and could not answer it after a restart until the primary was reachable. The README's primary-secondary example also used type: 'master' for a secondary zone.
  • bind: meta/argument_specs.yml declares all user-facing variables.
  • bind: aligned with roles/example and CONTRIBUTING.md: | bool on every boolean in the templates and tasks, | length > 0 in conditionals, ansible.builtin.service instead of ansible.builtin.systemd.

Test

New Molecule scenario extensions/molecule/bind on Rocky 8, 9 and 10 VMs: converge, verify, idempotence, verify, all green. The three hosts form a ring, each is the primary of its own zone and the secondary of a neighbour's zone (master/slave on Rocky 8, primary/secondary on Rocky 9/10); Rocky 9 and 10 also forward a third host's zone below the undelegated .example TLD. verify.yml asks the running named with dig:

  • own primary zone: NOERROR with aa
  • secondary zone after the transfer: NOERROR with aa and the primary's data, zone copy saved under /var/named (this check caught the slave regression on Rocky 8)
  • forward zone: NOERROR without aa, which needs the zone in validate-except
  • -x 10.1.2.3: NXDOMAIN with aa from the local empty zone
  • dnssec-failed.org: SERVFAIL; isc.org: ad flag

Before the scenario existed, verified by hand with the bind playbook against Rocky 8, 9 and 10 ubi-init containers:

  • Rocky 10 and Rocky 8 with bind__dnssec_validation: false: forward zones and dnssec-failed.org resolve
  • Rocky 8 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/
  • 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: 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, and the RHEL 8 assert since it moved into the always block.

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, static-stub 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.
@NavidSassan
NavidSassan merged commit 710a86a into main Oct 2, 2026
14 checks passed
@NavidSassan
NavidSassan deleted the fix/bind-dnssec branch October 2, 2026 09:31
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