Skip to content
ecdsa.com
← All articles

DNSSEC chain of trust: DS, DNSKEY, RRSIG and what breaks

A validating resolver trusts one key, the root's, and follows signed pointers down to the answer. Every DNSSEC outage is a broken link in that chain. This page walks a real chain with dig and delv, then breaks one on purpose in a lab to show what each mistake looks like.

Short answer. DNSSEC links zones with two record types. A DS record in the parent zone holds a hash of the child's key-signing key (DNSKEY, flags 257). That key signs the child's DNSKEY set, and the zone-signing key (flags 256) signs the actual records with RRSIGs. A validating resolver starts from the root key it trusts and checks every link down to the answer. If any link fails, it returns SERVFAIL instead of the data. Check a chain with delv ("fully validated") or dig +dnssec (the ad flag), compare the parent's DS with the child's DNSKEY using dnssec-dsfromkey, and use +cd to see whether the data is there but fails validation. Most outages come from key rollovers: a DS not updated after a KSK change, signatures left to expire, or keys swapped faster than caches expire.

The four records

RecordWhere it livesWhat it holds
DNSKEYIn the zone itselfPublic keys. Flags 257 marks a key-signing key (KSK, the "SEP" bit set), 256 a zone-signing key (ZSK)
RRSIGNext to each signed record setA signature over one record set (RRset) by one key, with inception and expiration times and the signing key's tag
DSIn the parent zone, at the delegationA digest of one of the child's DNSKEYs: key tag, algorithm, digest type, digest
NSEC / NSEC3In the zoneSigned proof that a name or type does not exist

The design splits keys by job. The KSK signs only the DNSKEY RRset, and its hash sits in the parent. Changing it means talking to the parent (registrar or registry). The ZSK signs everything else and can be changed without involving the parent. A zone can also use a single combined key (CSK) for both jobs, which is what BIND's built-in default dnssec-policy does (csk ... algorithm 13). The rules are the same, the key just has flags 257 and signs everything.

Walking a real chain with dig

www.isc.org, step by step, on 20 October 2026 (dig from BIND 9.20.10, queries sent to 1.1.1.1). The root first. Its DNSKEY set has one ZSK and two KSKs:

dig . DNSKEY +multiline (abridged)
.			2158 IN	DNSKEY 256 3 8 (
) ; ZSK; alg = RSASHA256 ; key id = 8763
.			2158 IN	DNSKEY 257 3 8 (
) ; KSK; alg = RSASHA256 ; key id = 20326
.			2158 IN	DNSKEY 257 3 8 (
) ; KSK; alg = RSASHA256 ; key id = 38696

Key 20326 is KSK-2017 and 38696 is KSK-2024. The root KSK rollover happened nine days before this capture. In ICANN's words, "On 11 October 2026 at 14:00 UTC, KSK-2024 began signing the DNS root zone as scheduled." The signature on the root DNSKEY set confirms it:

dig . DNSKEY +dnssec: the signature over the root key set
.			1587 IN	RRSIG DNSKEY 8 0 172800 (
20261101000000 20261011000000 38696 .

It is the only RRSIG over that set. The old key is still published until its planned revocation, but it no longer signs. A resolver that trusts only KSK-2017, because it never learned KSK-2024 through RFC 5011 automatic updates or a software update, can no longer validate anything.

Next, each delegation. The parent's DS names the child's KSK by key tag:

dig org. DS, dig org. DNSKEY, dig isc.org. DS, dig isc.org. DNSKEY (abridged)
org.			81000	IN	DS	26974 8 2 4FEDE294C53F438A158C41D39489CD78A86BEB0D8A0AEAFF14745C0D 16E1DE32
) ; ZSK; alg = RSASHA256 ; key id = 25488
) ; ZSK; alg = RSASHA256 ; key id = 11859
) ; ZSK; alg = RSASHA256 ; key id = 24060
) ; KSK; alg = RSASHA256 ; key id = 725
) ; KSK; alg = RSASHA256 ; key id = 26974
isc.org.		3600	IN	DS	7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2
) ; ZSK; alg = ECDSAP256SHA256 ; key id = 27566
) ; KSK; alg = ECDSAP256SHA256 ; key id = 7250

The root's DS for org. points to key 26974, which org. publishes as a KSK. The DS for isc.org. in org. points to key 7250, the KSK of isc.org. Note the algorithm change on the way down: org. signs with RSA (algorithm 8), isc.org with ECDSA P-256 (algorithm 13). Every zone chooses its own. Finally the answer and its signature by the isc.org ZSK, 27566:

dig www.isc.org. A +dnssec +multiline
www.isc.org.		300 IN A 151.101.3.42
www.isc.org.		300 IN A 151.101.67.42
www.isc.org.		300 IN A 151.101.131.42
www.isc.org.		300 IN A 151.101.195.42
www.isc.org.		300 IN RRSIG A 13 3 300 (
				20261103074926 20261020065242 27566 isc.org.
				qz5Qb4BFyOw4PRXBJjeK61LrPSKo6G2Ma1aFwNwZ57x5
				bFIg/d3EoMHtXopLYU949QQtMjcQ8BWULk7a629tLA== )

The RRSIG fields are: covered type A, algorithm 13, 3 labels, original TTL 300, expiration 2026-11-03 07:49:26 UTC, inception 2026-10-20 06:52:42 UTC, key tag 27566, signer isc.org.. A validating resolver that checks all of this sets the AD (authenticated data) flag:

dig www.isc.org A +dnssec @1.1.1.1
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1

ad is the resolver's claim, and it is only as trustworthy as the path to the resolver. To validate yourself, use delv, which does the whole walk locally against its built-in root trust anchors. +rtrace shows the fetches:

delv @1.1.1.1 www.isc.org A +rtrace (BIND 9.20.10, abridged)
;; fetch: www.isc.org/A
;; fetch: isc.org/DNSKEY
;; fetch: isc.org/DS
;; fetch: org/DNSKEY
;; fetch: org/DS
;; fetch: ./DNSKEY
; fully validated

dig +sigchase, which older guides recommend, is gone: dig 9.20 answers dig: +sigchase option no longer supported. Use delv. To do the same walk in the browser over DNS-over-HTTPS, the DNSSEC inspector fetches every link, verifies each RRSIG with its own code, and points at the link that breaks.

Algorithms 8, 13 and 15

The algorithm number appears in DNSKEY, DS and RRSIG records. RFC 9904 (November 2025) replaced RFC 8624 and moved the recommendations into the IANA registry itself; RFC 9905, published with it, forbids signing with the two SHA-1 algorithms:

NumberAlgorithmSigningValidation
5, 7RSA/SHA-1MUST NOTMUST implement; resolver operators treat it as unsupported
8RSA/SHA-256RECOMMENDED, MUST implementMUST implement
10RSA/SHA-512NOT RECOMMENDEDMUST implement
13ECDSA P-256/SHA-256RECOMMENDED, MUST implementMUST implement
14ECDSA P-384/SHA-384MAYRECOMMENDED
15Ed25519RECOMMENDEDRECOMMENDED
16Ed448MAYRECOMMENDED

For DS digests, SHA-256 (type 2) is the one to use. SHA-1 (type 1) is "MUST NOT" for delegations, though validators still have to support it. Of the ten zones we checked on 20 October 2026, com., net., nl., cloudflare.com, isc.org, ietf.org, nic.cz and sidn.nl sign with 13, while org. and de. use 8, as does the root. Signing the same zone with each algorithm shows why 13 is popular:

dnssec-keygen / dnssec-signzone 9.20.29: one zone, three algorithms
algorithm 8  RSASHA256         ZSK public key 260 bytes, RRSIG signature 256 bytes
algorithm 13 ECDSAP256SHA256   ZSK public key  64 bytes, RRSIG signature  64 bytes
algorithm 15 ED25519           ZSK public key  32 bytes, RRSIG signature  64 bytes

Small signatures keep responses under the UDP size limit (1232 bytes is the common EDNS buffer today), which avoids fragmentation and TCP fallback. Algorithm 15 has the smallest keys, but validator support is a RECOMMENDED, not a MUST, so check your audience's resolvers before you choose it.

Breaking a chain on purpose

To see failures without breaking a real domain, the lab below runs two BIND 9.20.29 instances on Debian 13. One is an authoritative server for a private root ., for lab. and for shop.lab., all signed with algorithm 13. The other is a validating resolver whose only trust anchor is the DS of that private root. The key and signing steps are ordinary BIND tooling:

Lab excerpt: keys, signing and the resolver's trust anchor
keygen() { dnssec-keygen -q -K keys -a ECDSAP256SHA256 -L 3600 "$@"; }
for z in . lab shop.lab; do keygen -fKSK "$z" >/dev/null; keygen "$z" >/dev/null; done

# the parent includes the child's dsset file before signing
cat auth/db.lab.unsigned auth/dsset-shop.lab. > auth/db.lab
dnssec-signzone -q -S -K keys -d auth -o lab -f auth/db.lab.signed auth/db.lab

read -r _ _ _ tag alg dt d1 d2 < auth/dsset-.
echo "trust-anchors { . static-ds $tag $alg $dt \"$d1$d2\"; };" > anchors.conf

1. Everything correct. The DS in lab. names the KSK of shop.lab., the resolver sets ad, and delv validates:

Lab, case 1
DS in lab.: shop.lab. 48794 13 2
DNSKEY in shop.lab.: ZSK; alg = ECDSAP256SHA256 ; key id = 6251
DNSKEY in shop.lab.: KSK; alg = ECDSAP256SHA256 ; key id = 48794
$ dig @127.0.0.1 www.shop.lab A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: NOERROR
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
www.shop.lab.		5	IN	A	192.0.2.10
$ delv @127.0.0.2 -a anchors.conf www.shop.lab A
; fully validated
www.shop.lab.		5	IN	A	192.0.2.10

2. KSK replaced in the child, DS in the parent not updated. This is the classic outage after a DNS hosting move or an automated KSK rollover that nobody completed at the registrar:

Lab, case 2
DS in lab.: shop.lab. 48794 13 2
DNSKEY in shop.lab.: KSK; alg = ECDSAP256SHA256 ; key id = 11943
DNSKEY in shop.lab.: ZSK; alg = ECDSAP256SHA256 ; key id = 6251
$ dig @127.0.0.1 www.shop.lab A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
$ dig @127.0.0.1 www.shop.lab A +dnssec +cd
;; ->>HEADER<<- opcode: QUERY, status: NOERROR
;; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
www.shop.lab.		5	IN	A	192.0.2.10
$ delv @127.0.0.2 -a anchors.conf www.shop.lab A
;; validating shop.lab/DNSKEY: no valid signature found (DS)
;; broken trust chain resolving 'www.shop.lab/A/IN': 127.0.0.2#53
;; resolution failed: broken trust chain

The DS still says 48794, the zone now has KSK 11943. +cd (checking disabled) returns the data, which proves the zone is up and only validation fails. That is the fastest way to tell a DNSSEC failure from an outage.

3. Signatures expired. The DS is fixed, but the zone was signed once and never re-signed. Here the lab signs it with an inception ten days ago and an expiration one hour ago:

Lab, case 3
RRSIG inception 20261010131152 expiration 20261020121152
$ dig @127.0.0.1 www.shop.lab A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDE: 7 (Signature Expired): (shop.lab/DNSKEY)
$ delv @127.0.0.2 -a anchors.conf www.shop.lab A
;; validating shop.lab/DNSKEY: no valid signature found (DS)
;; broken trust chain resolving 'www.shop.lab/A/IN': 127.0.0.2#53
;; resolution failed: broken trust chain

BIND attaches an Extended DNS Error (RFC 8914) that names the cause: EDE: 7 (Signature Expired). Static signing with dnssec-signzone gives 30-day signatures by default (the lab's first signing produced inception 20261020120639, expiration 20261119120639), so a cron job that stopped re-signing causes exactly this a month later. Let the DNS server sign and re-sign automatically instead: dnssec-policy in BIND, whose default refreshes 14-day signatures every 5 days, or the equivalent in Knot or PowerDNS.

4. ZSK swapped in one step. The zone replaces its ZSK and re-signs everything with the new key immediately. A resolver that cached the old DNSKEY set (TTL 3600) gets an A record signed by a key it does not have:

Lab, case 4
$ dig @127.0.0.1 www.shop.lab A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: NOERROR
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
www.shop.lab.		5	IN	A	192.0.2.10
resolver cache: DNSKEY flags 256 TTL 3600
resolver cache: DNSKEY flags 257 TTL 3600
(6 s later: the A record's 5 s TTL has expired, the cached DNSKEY set's 3600 s TTL has not)
$ dig @127.0.0.1 www.shop.lab A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
(resolver restarted, cache empty)
$ dig @127.0.0.1 www.shop.lab A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: NOERROR
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
www.shop.lab.		5	IN	A	192.0.2.10

The zone itself is valid: a fresh resolver validates it. But every resolver that cached the old key set fails until that cache entry expires. The outage is intermittent and resolver-dependent, the hardest kind to debug.

Rollovers that don't break

RFC 6781 describes the safe sequences. The rule behind all of them is that every resolver must be able to validate with whatever mix of old and new records it has cached.

  • ZSK, pre-publish. Add the new ZSK to the DNSKEY set while the old one still signs. Wait at least the DNSKEY TTL, so every cache has both keys. Switch signing to the new key. Wait for the longest TTL of the signed data and the old signatures to drain. Then remove the old key.
  • KSK, double-signature. Add the new KSK and sign the DNSKEY set with both. Submit the new DS to the parent. Wait until the parent publishes it and the old DS has expired from caches (TTL of the DS, often an hour or a day). Only then remove the old KSK.
  • Algorithm rollover (for example 8 to 13). New keys of the new algorithm must sign everything before the new DS appears, and the old algorithm must keep signing until the old DS is gone. RFC 6781 §4.1.4 has the full sequence.
  • Automate the parent side. CDS and CDNSKEY records (RFC 8078) let a parent that supports them pick up the new DS from the child. Without that, the DS step is manual at the registrar and is where rollovers stall.
  • Moving DNS providers. Either do a coordinated multi-signer move, or remove the DS at the registrar first, wait for its TTL, and re-enable DNSSEC with the new provider's key. Moving the NS records while the old DS stays is case 2 in production.

BIND's dnssec-policy and the key managers in Knot and PowerDNS implement these timings for you. The common failure is not the tool but the parent step: a DS that has to be pasted into a registrar's web form.

Diagnosing a SERVFAIL

  1. Is it DNSSEC? dig name +cd returns data while dig name gives SERVFAIL → validation failure. Look for an EDE line: 6 (DNSSEC Bogus), 7 (Signature Expired), 8 (Signature Not Yet Valid), 9 (DNSKEY Missing), 10 (RRSIGs Missing).
  2. Which link? Run delv name +rtrace and read the first failing step.
  3. DS versus DNSKEY: compute the DS from the child's keys and compare it with the parent:
DS in the parent vs DS computed from the child's DNSKEY (bind9-utils 9.20.29)
$ dig +short DS isc.org @1.1.1.1
7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2
$ dig +cd +noall +answer DNSKEY isc.org @1.1.1.1 | dnssec-dsfromkey -2 -f - isc.org
isc.org. IN DS 7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697CDA01CB2
$ dig +short DS dnssec-failed.org @1.1.1.1
42069 13 2 62726F6B656E20636861696E206F662074727573742073656E642068 656C7021
$ dig +cd +noall +answer DNSKEY dnssec-failed.org @1.1.1.1 | dnssec-dsfromkey -2 -f - dnssec-failed.org
dnssec-failed.org. IN DS 50719 13 2 D602C971A02AF89B7661B99D6DD1B9E2EC876617631D04F633902A42CD4D6BBD

isc.org matches. dnssec-failed.org is a deliberately broken test domain served by Comcast's name servers (dns101.comcast.net and others). Its DS names key 42069, which does not exist, and the digest is ASCII for "broken chain of trust send help!". Public resolvers reject it accordingly:

dig @1.1.1.1 dnssec-failed.org A
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 59477
; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for dnssec-failed.org.)
  1. Signature times: compare the RRSIG inception and expiration with the current UTC time. A signer or a validator with a wrong clock gives "not yet valid" or "expired".
  2. Resolver-dependent failures after a key change point at caching (case 4). Check the TTLs of the DNSKEY and DS sets and the time of the change.

What DNSSEC enables

A working chain of trust is the foundation for records that need authenticity, not just availability. DANE/TLSA pins TLS keys in DNS and is used for SMTP between mail servers. The TLSA record generator builds such records. CAA records are checked by CAs, and since 15 March 2026 the CA/Browser Forum Baseline Requirements require DNSSEC validation of CAA lookups where the zone is signed. A broken chain can therefore also block certificate issuance.

Sources

Real-world queries ran on 20 October 2026 in nicolaka/netshoot:v0.14 (BIND 9.20.10 dig and delv). The lab and dnssec-dsfromkey ran in debian:trixie-20261005-slim with BIND 9.20.29 from Debian.