From be3e4c83d0219d615953db53ba1bb5d63f67a054 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Thu, 6 Nov 2025 17:32:51 +0100 Subject: [PATCH 1/2] Test retransfer with NSEC3 policy If the primary has been updated, but the secondary has not been notified, the journal will go out of date. An 'rndc retransfer' causes the zone to force an AXFR, removing and rebuilding zone and journal files. This test reproduces a bug that in such scenario, an NSEC3 signed zone falls back to NSEC. --- bin/tests/system/nsec3/common.py | 4 +- bin/tests/system/nsec3/ns2/named.conf.j2 | 10 ++ .../system/nsec3/ns2/retransfer.kasp.db.j2 | 31 +++++ .../system/nsec3/ns3/named-retransfer.conf.j2 | 49 +++++++ bin/tests/system/nsec3/ns3/named.conf.j2 | 1 + bin/tests/system/nsec3/ns4/named.conf.j2 | 39 ++++++ .../system/nsec3/tests_nsec3_retransfer.py | 129 ++++++++++++++++++ 7 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 bin/tests/system/nsec3/ns2/retransfer.kasp.db.j2 create mode 100644 bin/tests/system/nsec3/ns3/named-retransfer.conf.j2 create mode 100644 bin/tests/system/nsec3/ns4/named.conf.j2 create mode 100644 bin/tests/system/nsec3/tests_nsec3_retransfer.py diff --git a/bin/tests/system/nsec3/common.py b/bin/tests/system/nsec3/common.py index 678cc4cbed..c7312cd324 100644 --- a/bin/tests/system/nsec3/common.py +++ b/bin/tests/system/nsec3/common.py @@ -35,9 +35,7 @@ pytestmark = pytest.mark.extra_artifacts( "ns*/*.jnl", "ns*/*.signed", "ns*/keygen.out.*", - "ns3/named-common.conf", - "ns3/named-fips.conf", - "ns3/named-rsasha1.conf", + "ns3/named-*.conf", ] ) diff --git a/bin/tests/system/nsec3/ns2/named.conf.j2 b/bin/tests/system/nsec3/ns2/named.conf.j2 index 904abbf81d..ec680db677 100644 --- a/bin/tests/system/nsec3/ns2/named.conf.j2 +++ b/bin/tests/system/nsec3/ns2/named.conf.j2 @@ -46,3 +46,13 @@ zone "nsec3-xfr-inline.kasp" { dnssec-policy "nsec3"; }; {% endif %}{# nsec3-xfr-inline.kasp #} + +{% if "retransfer.kasp" in zones %} +zone "retransfer.kasp" { + type primary; + file "retransfer.kasp.db"; + notify explicit; + also-notify { 10.53.0.3; }; + allow-transfer { any; }; +}; +{% endif %}{# nsec3-xfr-inline.kasp #} diff --git a/bin/tests/system/nsec3/ns2/retransfer.kasp.db.j2 b/bin/tests/system/nsec3/ns2/retransfer.kasp.db.j2 new file mode 100644 index 0000000000..f8a138c603 --- /dev/null +++ b/bin/tests/system/nsec3/ns2/retransfer.kasp.db.j2 @@ -0,0 +1,31 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +{% set serial = serial | default(1) %} + +$ORIGIN retransfer.kasp. +$TTL 300 + +retransfer.kasp. IN SOA mname1. . ( + @serial@ ; serial + 20 ; refresh (20 seconds) + 20 ; retry (20 seconds) + 1814400 ; expire (3 weeks) + 3600 ; minimum (1 hour) + ) + + NS ns2 +ns2 A 10.53.0.2 +ns3 A 10.53.0.3 + +a A 10.0.0.1 +b A 10.0.0.2 +c A 10.0.0.3 diff --git a/bin/tests/system/nsec3/ns3/named-retransfer.conf.j2 b/bin/tests/system/nsec3/ns3/named-retransfer.conf.j2 new file mode 100644 index 0000000000..e521f96621 --- /dev/null +++ b/bin/tests/system/nsec3/ns3/named-retransfer.conf.j2 @@ -0,0 +1,49 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +remote-servers "ns2" { + 10.53.0.2 port @PORT@; +}; + +remote-servers "ns4" { + 10.53.0.4 port @PORT@; +}; + +dnssec-policy "nsec3rsa256" { + cdnskey no; + keys { + ksk lifetime unlimited algorithm RSASHA256 2048; + zsk lifetime P90D algorithm RSASHA256 2048; + }; + max-zone-ttl P2D; + signatures-refresh P8D; + + nsec3param; +}; + +{% if "retransfer.kasp" in zones %} +zone "retransfer.kasp" { + type secondary; + primaries { "ns2"; }; + file "retransfer.kasp.db"; + allow-transfer { any; }; + allow-notify { any; }; + also-notify { "ns4"; }; + notify explicit; + + dnssec-policy "nsec3rsa256"; + inline-signing yes; + sig-signing-signatures 100; + checkds no; +}; +{% endif %}{# retransfer.kasp #} diff --git a/bin/tests/system/nsec3/ns3/named.conf.j2 b/bin/tests/system/nsec3/ns3/named.conf.j2 index 7dd06ad83c..7aa4f17194 100644 --- a/bin/tests/system/nsec3/ns3/named.conf.j2 +++ b/bin/tests/system/nsec3/ns3/named.conf.j2 @@ -15,6 +15,7 @@ include "named-common.conf"; include "named-fips.conf"; +include "named-retransfer.conf"; {% if RSASHA1_SUPPORTED == "1" %} include "named-rsasha1.conf"; diff --git a/bin/tests/system/nsec3/ns4/named.conf.j2 b/bin/tests/system/nsec3/ns4/named.conf.j2 new file mode 100644 index 0000000000..59a126b6f4 --- /dev/null +++ b/bin/tests/system/nsec3/ns4/named.conf.j2 @@ -0,0 +1,39 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +// NS4 + +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + listen-on-v6 { none; }; + allow-transfer { any; }; + recursion no; + dnssec-validation no; +}; + +remote-servers "ns3" { + 10.53.0.3 port @PORT@; +}; + +zone "retransfer.kasp" { + type secondary; + file "retransfer.kasp.db"; + primaries { "ns3"; }; + allow-notify { any; }; + notify no; +}; diff --git a/bin/tests/system/nsec3/tests_nsec3_retransfer.py b/bin/tests/system/nsec3/tests_nsec3_retransfer.py new file mode 100644 index 0000000000..96a23761b5 --- /dev/null +++ b/bin/tests/system/nsec3/tests_nsec3_retransfer.py @@ -0,0 +1,129 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +# pylint: disable=redefined-outer-name,unused-import + +import os +import shutil + +from datetime import timedelta + +import dns.update +import pytest + +pytest.importorskip("dns", minversion="2.0.0") +import isctest +import isctest.mark +from isctest.vars.algorithms import RSASHA256 +from nsec3.common import ( + pytestmark, + check_auth_nsec3, + check_nsec3param, +) + +DNSKEY_TTL = int(timedelta(hours=1).total_seconds()) +ZSK_LIFETIME = int(timedelta(days=90).total_seconds()) + +# include the following zones when rendering named configs +ZONES = { + "retransfer.kasp", +} + + +def bootstrap(): + return { + "zones": ZONES, + } + + +def perform_nsec3_tests(server, params): + # Get test parameters. + zone = params["zone"] + fqdn = f"{zone}." + policy = params["policy"] + keydir = server.identifier + minimum = params.get("soa-minimum", 3600) + expected = isctest.kasp.policy_to_properties( + ttl=DNSKEY_TTL, keys=params["key-properties"] + ) + + iterations = 0 + optout = 0 + saltlen = 0 + + match = f"{fqdn} {minimum} IN NSEC3PARAM 1 0 {iterations}" + + # Test case. + isctest.log.info(f"check nsec3 case zone {zone} policy {policy}") + + # First make sure the zone is properly signed. + isctest.kasp.wait_keymgr_done(server, zone) + + keys = isctest.kasp.keydir_to_keylist(zone, keydir) + ksks = [k for k in keys if k.is_ksk()] + zsks = [k for k in keys if k.is_zsk()] + isctest.kasp.check_keys(zone, keys, expected) + isctest.kasp.check_dnssec_verify(server, zone) + isctest.kasp.check_apex(server, zone, ksks, zsks) + + query = isctest.query.create(fqdn, dns.rdatatype.NSEC3PARAM) + response = isctest.query.tcp(query, server.ip, server.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NOERROR + + salt = check_nsec3param(response, match, saltlen) + + query = isctest.query.create(f"nosuchname.{fqdn}", dns.rdatatype.A) + response = isctest.query.tcp(query, server.ip, server.ports.dns, timeout=3) + assert response.rcode() == dns.rcode.NXDOMAIN + check_auth_nsec3(response, iterations, optout, salt) + + return salt + + +def test_nsec3_retransfer(servers, templates): + ns2 = servers["ns2"] + ns3 = servers["ns3"] + + params = { + "zone": "retransfer.kasp", + "policy": "nsec3rsa256", + "key-properties": [ + f"ksk 0 {RSASHA256.number} 2048 goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden", + f"zsk {ZSK_LIFETIME} {RSASHA256.number} 2048 goal:omnipresent dnskey:rumoured zrrsig:rumoured", + ], + } + + zone = params["zone"] + salt = perform_nsec3_tests(ns3, params) + + # Stop primary. + ns2.stop() + + # Update the zone. + serial = 10 + templates.render(f"{ns2.identifier}/{zone}.db", {"serial": serial}) + + with ns2.watch_log_from_here() as watcher: + ns2.start(["--noclean", "--restart", "--port", os.environ["PORT"]]) + watcher.wait_for_line("all zones loaded") + + # Test NSEC3 and NSEC3PARAM is the same after retransfer. + isctest.log.info(f"check zone {zone} after retransfer has salt {salt}") + prevsalt = salt + + # Retransfer zone, NSEC3 should stay the same. + with ns3.watch_log_from_here() as watcher: + ns3.rndc(f"retransfer {zone}") + # When sending notifies, the zone should be up to date. + watcher.wait_for_line(f"zone {zone}/IN (signed): sending notify to 10.53.0.4") + + salt = perform_nsec3_tests(ns3, params) + assert prevsalt == salt From 6dcb9ce77ff774ffab3139c052b0f177338953d0 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 7 Nov 2025 15:56:54 +0100 Subject: [PATCH 2/2] Skip private records when syncing secure db When synchronizing the secure database, we skip DNSSEC records that BIND 9 maintains with inline-signing. We should also skip private RDATA type records that are used to track the current state of a zone-signing process. --- lib/dns/zone.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/dns/zone.c b/lib/dns/zone.c index a46969b3e8..2b70bdc93a 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -16312,6 +16312,17 @@ sync_secure_db(dns_zone_t *seczone, dns_zone_t *raw, dns_db_t *secdb, ISC_LIST_FOREACH(diff->tuples, tuple, link) { dns_difftuplelist_t *al = &add, *dl = &del; + /* + * Skip private records that BIND maintains with inline-signing. + */ + if (seczone->privatetype != 0 && + tuple->rdata.type == seczone->privatetype) + { + ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_difftuple_free(&tuple); + continue; + } + /* * Skip DNSSEC records that BIND maintains with inline-signing. */