fix: usr: Fix bug where zone switches from NSEC3 to NSEC after retransfer

When a zone is re-transferred, but the zone journal on an inline-signing secondary is out of sync, the zone could fall back to using NSEC records instead of NSEC3. This has been fixed.

Closes #5527

Merge branch '5527-retransfer-nsec3-bug' into 'main'

See merge request isc-projects/bind9!11226
This commit is contained in:
Matthijs Mekking 2025-11-24 13:23:21 +00:00
commit ddd1040761
8 changed files with 271 additions and 3 deletions

View file

@ -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",
]
)

View file

@ -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 #}

View file

@ -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

View file

@ -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 #}

View file

@ -15,6 +15,7 @@
include "named-common.conf";
include "named-fips.conf";
include "named-retransfer.conf";
{% if RSASHA1_SUPPORTED == "1" %}
include "named-rsasha1.conf";

View file

@ -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;
};

View file

@ -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

View file

@ -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.
*/