Module 07 — DNSSEC

Updated 20 August 2026

Module 07 · DNSSEC

You have met ad, cd and do flags, an RRSIG in a +trace, a DS record at the .se delegation, and secure: no in rndc zonestatus — every one deferred to "Module 07". This is it. You will sign the lab.internal zone from Module 05, validate it, then break it on purpose and watch it fail the way DNSSEC always fails: as a bare SERVFAIL with no explanation.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01–05. You need RRsets (02 A1), the SOA and delegation (02, 03), EDNS and the do bit (04 B3), and a working BIND primary (05).


Part A · What DNSSEC is for

A1 · The problem, restated precisely

The analogy. Think of anyone being able to print a letter on your company letterhead.

You can make the letterhead harder to copy — fancier paper, a watermark, a serial number. Each one makes forgery less likely, and none of them makes it impossible.

A signature the bank can check against a specimen on file is a different kind of protection altogether. That is the step DNSSEC takes.

Module 04 C ended on an uncomfortable note: source-port randomisation, 0x20 and DNS cookies all make forgery unlikely. None makes it impossible, because all of them are guessing games and the attacker gets unlimited attempts.

DNSSEC changes the question from "could this be forged?" to "is this signed by the key the parent vouched for?" — and that is a question with a definite answer.

The zone owner signs each RRset with a private key. The public key is published in DNS. The parent zone publishes a hash of that public key, and the parent's own key is vouched for by its parent, up to the root — whose key is the one thing every validating resolver is configured with in advance.

So a forged answer needs the zone's private key. Guessing is off the table.

Three things DNSSEC does not do, and interviewers ask about all three.
  1. It does not encrypt anything. Every query and response stays in plaintext, readable by anyone on the path. DNSSEC is authentication and integrity, never confidentiality. Encryption is DoT/DoH/DoQ — Module 09
  2. It does not protect the last hop. It secures the path from the zone to the validating resolver. Between that resolver and your laptop there is nothing but the ad bit, which anything could set. If you do not trust the channel to your resolver, ad is worth nothing
  3. It does not say a domain is trustworthy. A phishing site can be perfectly signed. DNSSEC proves the data came from the domain's owner, not that the owner is honest

A2 · The chain of trust

The analogy. Think of a passport at an immigration desk.

The officer does not know you. He trusts the passport because he trusts the country that issued it — and he trusts that country because of an agreement his own government already holds.

Every level vouches for the one below it, all the way up to something he was given in advance and simply accepts. In DNS, that pre-agreed thing is the root's key, and it is shipped with every resolver.

Diagram source
flowchart TD
    ROOT["ROOT ZONE<br>DNSKEY - the trust anchor<br>shipped with every resolver"]
    ROOT -->|"root signs a DS for se"| DSSE["DS record for se<br>lives in the ROOT zone"]
    DSSE -->|"hash of"| SEKEY["se DNSKEY<br>lives in the SE zone"]
    SEKEY -->|"se signs a DS for the child"| DSCH["DS record for example.se<br>lives in the SE zone"]
    DSCH -->|"hash of"| CHKEY["example.se DNSKEY"]
    CHKEY -->|"signs"| RRSIG["RRSIG over every RRset<br>A, MX, TXT, NS ..."]
    RRSIG --> DATA["the records you asked for"]
    style ROOT fill:#8b5cf6,color:#fff
    style DSCH fill:#f59e0b,color:#fff
    style DATA fill:#22c55e,color:#fff
Read the diagram for one structural fact: the DS record lives in the PARENT, and everything else lives in the child. That single split is the reason DNSSEC is operationally hard.

Your keys, your signatures and your DNSKEY records are all yours to change whenever you like. The DS is not — it sits in your parent's zone and is published through your registrar. It is the same problem as glue records in Module 03 A4: a piece of your configuration that lives in somebody else's system, on somebody else's change cadence.

Get the ordering wrong — change keys before the parent's DS catches up — and your entire domain goes dark for every validating resolver in the world. Not degraded. Dark.

🧪 Exercise A2.1 — See a real chain, and see where it stops
bash
# the root is signed
dig . DNSKEY +noall +answer +dnssec | head -3

# .se is signed, and the ROOT publishes its DS
dig se DS +noall +answer

# does seamless.se have a DS in .se?
dig seamless.se DS +noall +answer +comments | head -4
Expected result — click to reveal
plain text
$ dig se DS +noall +answer
se.			86400	IN	DS	35734 8 2 ADD3C9E3D19A0C4B2A1F3EE43F4F1CBA6C6E9F...

$ dig seamless.se DS +noall +answer +comments
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21993
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

.se has a DS in the root, so .se is signed and the root vouches for it. That is the DS record you saw in the +trace output in Module 03 A3.1 and could not read.

seamless.se returns NOERROR with ANSWER: 0 — NODATA. There is no DS, so the chain of trust stops at .se. The zone is unsigned, and a validating resolver will treat it as insecure rather than bogus — which is a critical distinction: unsigned is fine, signed-and-broken is fatal.

This is the same finding as Module 02 D1.1, where DNSKEY returned (none). Now you know exactly what it means: nobody has signed seamless.se, and nobody has published a DS for it at the registrar. Both steps are required, in that order.

And note the ad flag on that second response. The resolver validated the proof of non-existence of the DS record — .se cryptographically asserts "there is no DS here", and the resolver checked that assertion. Even the absence of DNSSEC is authenticated on a signed parent. That is what NSEC/NSEC3 in B4 are for.


Part B · The four records

B1 · DNSKEY, and the ZSK / KSK split

The analogy. Think of the office rubber stamp versus the director's registered signature.

The rubber stamp is used hundreds of times a day and can be replaced whenever you like — you just order a new one. Nobody outside needs to be told.

The director's signature is lodged at the bank. Changing it means an appointment, paperwork, and waiting for the bank to update its records.

So you use the stamp for everything and change the signature almost never. That is the entire reason for two keys — not strength, but how expensive each one is to change.

plain text
lab.internal. IN DNSKEY 257 3 13 3PqgQ08vhhPv0cv6FZy2022cURmQ2LHEgNhvOCzKNMY...
lab.internal. IN DNSKEY 256 3 13 FHswsH8ibj4a6OWN5DRPJ6d6F6MRN23JqHb8rAJi0vX...
                        │   │ │
                        │   │ └── algorithm 13 = ECDSAP256SHA256
                        │   └──── protocol, always 3
                        └──────── flags: 257 = KSK, 256 = ZSK
KeyJob
ZSK — flags 256Signs every RRset in the zone. Used constantly, rotated often, purely your business
KSK — flags 257Signs only the DNSKEY RRset. Its hash is the DS in the parent. Rotated rarely, because rotating it means a registrar change
Why split them at all? Because one of the two keys is expensive to change and the other is not.

The DS in the parent points at the KSK. Changing the KSK therefore requires coordinating with your registrar and waiting for the parent's TTL — slow, manual, and risky.

The ZSK is referenced by nothing outside your own zone, so you can roll it whenever you like, with no external coordination at all.

The split lets you rotate the key that does all the work, frequently and safely, while touching the externally-visible key almost never. That is the whole design rationale, and it is why "why two keys?" is such a common interview question — it separates people who have signed a zone from people who have read about it.

Use algorithm 13 (ECDSAP256SHA256), not 8 (RSASHA256). ECDSA signatures are roughly a quarter the size of equivalent RSA ones, and Module 04 B4 explains exactly why that matters: response size decides whether ordinary queries need TCP. A zone signed with RSA-2048 can push a routine answer past 1232 bytes; the same zone on ECDSA stays comfortably under.

B2 · RRSIG — the signature

The analogy. Think of a signed letter with an expiry date printed on it.

After that date the letter is worthless — even though the signature is genuine, the contents are correct, and nobody has touched it. Time alone invalidated it.

That is why the classic DNSSEC outage begins with someone saying "but we didn't change anything". Not changing anything is precisely how it happens.

plain text
lab.internal. 300 IN RRSIG A 13 2 300 20260919024216 20260820024216 63231 lab.internal. /zV0OBf6xN...
                            │  │ │  │        │              │          │
                            │  │ │  │        │              │          └── key tag: WHICH key
                            │  │ │  │        │              └───────────── inception
                            │  │ │  │        └──────────────────────────── EXPIRATION
                            │  │ │  └───────────────────────────────────── original TTL
                            │  │ └──────────────────────────────────────── labels
                            │  └────────────────────────────────────────── algorithm 13
                            └───────────────────────────────────────────── covers type A
Look at the expiration field, because it is the single biggest cause of real DNSSEC outages.

An RRSIG expires on a wall-clock date, whether or not anything changed. A zone you have not touched in months will go bogus on a fixed date if nothing re-signs it. Nothing warns you. Nothing degrades first. On that date, every validating resolver in the world starts returning SERVFAIL for your domain.

The default signature lifetime in dnssec-signzone is 30 days. Re-signing is not maintenance you do when you change records — it is a recurring job that must run whether or not anything changes. Modern BIND with dnssec-policy handles it automatically; hand-run dnssec-signzone in a script does not unless you make it.

This is why "our domain broke and we hadn't changed anything" is the classic DNSSEC incident. Nobody changed anything. That is precisely the problem.

And note the unit being signed: RRSIG A covers the whole A RRset, not one record. Module 02 A1's rule that DNS operates on RRsets is why — you could not sign individual records and still allow the set to be reassembled, so the signature covers the set as a unit. Add one address to that set and the signature over it must be recomputed.

The analogy. Think of your signature specimen held at the bank — not in your own filing cabinet.

You can reorganise your office however you like. To change the specimen you have to go to the bank, on their hours, and wait for their records to catch up.

And the order matters enormously: register the new signature before you start using it, and stop using the old one only after the bank has caught up. Get that backwards and every cheque bounces.

plain text
lab.internal. IN DS 50540 13 2 E2DF051E442951F9B46525F79FA1E11B3346266D24321D376E04780B321472A0
                    │     │  │  └── the hash of the KSK
                    │     │  └───── digest type 2 = SHA-256
                    │     └──────── algorithm 13
                    └────────────── key tag, matching the KSK
The DS is generated by you and published by your parent. You produce it from your KSK with dnssec-dsfromkey, then hand it to your registrar, who puts it in the TLD zone.

Two consequences you will meet:

  • Signing a zone does nothing until the DS is published. A signed zone with no DS is simply insecure — resolvers do not validate it, and nothing breaks. This is the safe state to be in while you test
  • Removing the DS is how you safely un-sign. Delete the DS, wait out the parent's TTL, then remove the signatures. Do it the other way round and the domain goes dark

B4 · NSEC / NSEC3 — proving that something does not exist

The analogy. Think of proving a name is not in a printed register.

You cannot point at an entry that is not there. So instead you show the two entries either side of where it would have been: "look, we go straight from Ahmad to Chen — there is no Bakar."

It is a genuine proof. It also has an unavoidable side effect: you just told the reader about Ahmad and Chen. Repeat that enough times and a stranger can copy out the whole register without ever being handed it.

Signing the records you have is easy. Proving you do not have a record is the hard part — you cannot sign a record that does not exist, and you cannot pre-sign an answer for every possible name.

NSEC solves it by signing the gaps. Sort every name in the zone alphabetically, then for each one publish a signed record saying "the next name after api is mail". A resolver asking for dev receives that record, sees dev falls between api and mail, and has cryptographic proof that dev does not exist.

And that is exactly the zone-walking technique from Module 02 D3. Follow api → mail → ns1 → www and you have enumerated the entire zone with no guessing, because the proof of non-existence necessarily reveals the neighbours.

NSEC3 hashes the names before sorting them, so walking yields hashes rather than names. It raises the cost — you must crack the hashes offline — but with modern hardware and a dictionary it does not eliminate the problem, which is why RFC 9276 recommends 0 additional iterations and an empty salt: the extra work was punishing your own servers far more than the attacker.

The honest summary: if you need names to be secret, do not put them in a signed public zone. Use split-horizon (Module 08). NSEC3 is obfuscation, not a control.


Part C · Signing a zone, for real

C1 · Generate the keys

The analogy. Think of cutting two keys: one for the desk drawer you open every day, one for the safe.

And note what you get for each: a key and a copy of its shape you can show people. The shape is safe to publish. The key itself opens the door, and it should never leave the building.

🧪 Exercise C1.1 — Make a KSK and a ZSK
bash
cd /tmp/lab && mkdir -p keys && cd keys
dnssec-keygen -a ECDSAP256SHA256 -f KSK -n ZONE lab.internal
dnssec-keygen -a ECDSAP256SHA256        -n ZONE lab.internal
ls -1
grep -h -v '^;' *.key
Expected result — click to reveal
plain text
Klab.internal.+013+50540        <- KSK
Klab.internal.+013+63231        <- ZSK

Klab.internal.+013+50540.key
Klab.internal.+013+50540.private
Klab.internal.+013+63231.key
Klab.internal.+013+63231.private

lab.internal. IN DNSKEY 257 3 13 3PqgQ08vhhPv0cv6FZy2022cURmQ2LHEgNhvOCzKNMYIsCq2g0rf8BNP MKjruVY9MtdNZ2ZQsxAAB4HSvIKfIA==
lab.internal. IN DNSKEY 256 3 13 FHswsH8ibj4a6OWN5DRPJ6d6F6MRN23JqHb8rAJi0vXuaJqPMPWPamyS MWZMyZ10PgiLM8hiIDyMVRsPPmxqwA==

Read the filenames — they encode everything. Klab.internal.+013+50540: zone name, algorithm 013, key tag 50540. That key tag is what the RRSIG and the DS both point at, so when a signature says key tag 63231 you can tell at a glance it was the ZSK.

Flags 257 versus 256 is the only difference between the two keys in the record. The maths is identical; the flag is a declaration of intent.

Two files per key, and the .private one is the crown jewels. Anyone holding it can sign anything for your zone. It should never be in Git, never in a container image, and on a serious deployment it lives in an HSM. -f KSK on the first command is what sets the 257 flag.


C2 · Sign it

The analogy. Think of counter-signing every page of a contract, not just the front.

It takes longer and the document gets noticeably thicker — which is exactly what happens to a signed zone. Roughly three times the paper, and that thickness is the reason the transport chapter suddenly matters.

🧪 Exercise C2.1 — Add the keys to the zone, sign it, and read the output
bash
cd /tmp/lab
cat keys/*.key >> p/zone.db            # the DNSKEY RRset must be IN the zone
# bump the serial, as Module 05 C3 drilled into you
sed -i 's|2026081702 ; serial|2026081703 ; serial|' p/zone.db

cd /tmp/lab/p
dnssec-signzone -A -3 "$(head -c 8 /dev/urandom | od -A n -t x1 | tr -d ' \n')" \
                -N INCREMENT -o lab.internal -t -K /tmp/lab/keys zone.db
Expected result — click to reveal
plain text
Zone fully signed:
Algorithm: ECDSAP256SHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                            ZSKs: 1 active, 0 stand-by, 0 revoked
zone.db.signed
Signatures generated:                       15
Signatures retained:                         0
Signing time in seconds:                 0.004

And inside zone.db.signed:

plain text
lab.internal.		300	IN SOA	ns1.lab.internal. hostmaster.lab.internal. (
					2026081704 ; serial
					... )
			300	RRSIG	SOA 13 2 300 (
					20260919024216 20260820024216 63231 lab.internal.
					uO6F+05v9Si/GfhRAlxUtHMwyC4dkFX9haZD ... )
			300	NS	ns1.lab.internal.
			300	RRSIG	NS 13 2 300 (
					20260919024216 20260820024216 63231 lab.internal. ... )
			300	A	192.0.2.99

Fifteen signatures for a nine-record zone. One RRSIG per RRset, plus the NSEC3 chain and its signatures. This is why signing roughly triples response size — the claim made in Module 04 D2 and now visible.

-N INCREMENT bumped the serial for you, from ...03 to ...04. Signing is a zone change, and Module 05 C3's rule applies: without a serial bump your secondaries would keep serving the unsigned copy.

Read the two dates in every RRSIG: 20260919024216 20260820024216. Expiration first, inception second. Thirty days. Put that date in your calendar right now — or better, use dnssec-policy so a machine owns it.

-3 <salt> selects NSEC3. Per RFC 9276 the modern recommendation is 0 extra iterations and an empty salt; the salt here exists only to show the flag. The iterations were always a bigger cost to your own servers than to an attacker.

-t printed the statistics; -K said where the keys live; -A kept the NSEC3 opt-out behaviour off. The output file is zone.db.signed, and that is what named must now serve — not the original.


C3 · Serve it, and publish the DS

The analogy. Think of lodging your signature at the bank — but only after your office is actually able to sign things.

Lodge it first and every cheque you write bounces, because the bank is now checking for a signature nobody in your office is producing yet. The order is not a preference. It is the difference between a quiet rollout and a total outage.

bash
# point named at the SIGNED file
sed -i 's|file "/tmp/lab/p/zone.db";|file "/tmp/lab/p/zone.db.signed";|' /tmp/lab/p/named.conf
named-checkconf -z /tmp/lab/p/named.conf

# the DS record, to hand to the registrar
cat /tmp/lab/p/dsset-lab.internal.
🧪 Exercise C3.1 — Confirm the zone loaded as signed, and produce the DS
bash
named-checkconf -z /tmp/lab/p/named.conf
cat /tmp/lab/p/dsset-lab.internal.
Expected result — click to reveal
plain text
zone lab.internal/IN: loaded serial 2026081704 (DNSSEC signed)

lab.internal.		IN DS 50540 13 2 E2DF051E442951F9B46525F79FA1E11B3346266D24321D376E04780B321472A0

(DNSSEC signed) is BIND confirming it recognised the signatures. If that suffix is missing, you pointed named at the unsigned file — the single most common mistake at this step, because both files sit in the same directory with almost the same name.

The DS names key tag 50540 — the KSK, not the ZSK. That is the whole point of the split from B1.

Now the step that is not a command: you send that DS line to your registrar, through their control panel or API. Until the parent publishes it, resolvers see an unsigned zone and validate nothing.

And the ordering rule, which is the one that causes outages: publish the DS after the signed zone is live and serving correctly on every nameserver. If the DS appears first, resolvers start demanding valid signatures from servers that are not yet serving them, and your domain goes bogus — SERVFAIL everywhere — for the length of the parent's TTL.


C4 · Validate it

The analogy. Think of the teller comparing your signature against the specimen on file.

Notice who does the checking: the teller, not you. The person presenting the document never verifies their own signature — which is why an authoritative server will happily serve data whose signature no longer matches, and why you must test through something that actually validates.

lab.internal has no real parent, so there is nowhere to publish a DS. Instead, configure a validating resolver with the KSK as a trust anchor directly — which is exactly what every resolver does for the root.

bash
mkdir -p /tmp/lab/r
KSKREC=$(grep -v '^;' /tmp/lab/keys/Klab.internal.+013+50540.key \
         | sed 's/.*DNSKEY 257 3 13 //' | tr -d '\n' | tr -s ' ')

cat > /tmp/lab/r/named.conf <<EOF
options {
    directory "/tmp/lab/r";
    pid-file  "/tmp/lab/r/named.pid";
    listen-on port 5303 { 127.0.0.1; };
    listen-on-v6 { none; };
    recursion yes;
    allow-recursion { 127.0.0.1; };
    dnssec-validation yes;
};
trust-anchors {
    lab.internal. static-key 257 3 13 "$KSKREC";
};
zone "lab.internal" { type forward; forward only; forwarders { 127.0.0.1 port 5301; }; };
EOF

named-checkconf /tmp/lab/r/named.conf
setsid named -c /tmp/lab/r/named.conf > /tmp/lab/r.log 2>&1 < /dev/null
🧪 Exercise C4.1 — Get a validated answer, and see the ad flag earned
bash
dig @127.0.0.1 -p 5303 lab.internal A +dnssec +noall +comments +answer
Expected result — click to reveal
plain text
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62290
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; COOKIE: b94da364249d9917010000006a8677ba19ec6af7f5026ca5 (good)
;; ANSWER SECTION:
lab.internal.		300	IN	A	192.0.2.99
lab.internal.		300	IN	RRSIG	A 13 2 300 20260919024216 20260820024216 63231 lab.internal. /zV0OBf6xNANBvrriInY4z3im2KMXTW1Mdj3mWVUD1ZiR1dT6wVxI1ej 5wEI0RVnY9Ayy4q4R28xGwIhVVTt/A==

ad is set, and this time you know precisely what it cost. The resolver fetched the DNSKEY RRset, checked it against the trust anchor, verified the RRSIG over the A record with the ZSK, and only then set that one bit.

flags: do in the OPT record is what asked for the signature — Module 04 A2's DNSSEC-OK bit, which is why DNSSEC cannot work without EDNS.

ANSWER: 2 for a single address: the A record and its RRSIG. That doubling, across every RRset, is the size cost.

A note on delv, because it will confuse you here. delv validates using the system's trust anchors, not your resolver's, so against this lab zone it reports broken trust chain resolving 'lab.internal/DNSKEY/IN' — correctly, since the system has never heard of lab.internal. On a real signed domain (dig/delv against org or se) delv prints ; fully validated and is the better tool. The error is a true statement about your machine's trust configuration, not a fault in the zone.

🎯 Interview questions — How DNSSEC works

Q. Explain DNSSEC and the chain of trust.

The zone owner signs each RRset with a private key, producing RRSIG records. The public key is published as DNSKEY. The parent zone publishes a DS record — a hash of the child's key-signing key — and the parent's own keys are vouched for by its parent, up to the root, whose key is the trust anchor every validating resolver is preconfigured with.

So a resolver walks down from the root, and at each delegation the parent's signed DS authenticates the child's key.

The three things I would add, because they are where candidates separate. It provides authentication and integrity, not encryption — queries stay in plaintext. It protects the path to the validating resolver, not the last hop to your laptop, where only the ad bit exists and anything could set it. And it says nothing about whether a domain is trustworthy — a phishing site can be perfectly signed.

Q. Why are there two keys, a KSK and a ZSK?

Because one of them is expensive to change and the other is not. The DS in the parent points at the KSK, so changing the KSK means a registrar transaction and waiting out the parent's TTL. The ZSK is referenced only inside your own zone, so it can be rolled freely with no external coordination.

The KSK signs only the DNSKEY RRset; the ZSK signs everything else.

The design consequence worth stating: the split lets you rotate the key that does all the signing work frequently and safely, while touching the externally-visible key rarely. It is a separation of operational cadence, not of cryptographic strength — the algorithms are identical.

Q. How does DNSSEC prove that a name does not exist?

With NSEC or NSEC3 records, which sign the gaps between names. Sort the zone's names; publish a signed record saying "the next name after api is mail". A query for dev gets that record back and can verify cryptographically that nothing exists between the two.

NSEC3 hashes the names before sorting, so a walk yields hashes rather than plaintext.

The consequence that makes this more than trivia: plain NSEC allows complete zone enumeration — follow the chain and you have every name, no guessing. That is one of the six enumeration methods from Module 02 D3. NSEC3 raises the cost but does not eliminate it, since hashes can be cracked offline, and RFC 9276 now recommends zero extra iterations because the added work punished the zone's own servers more than the attacker. If names must be secret, the answer is split-horizon, not NSEC3.


Part D · How DNSSEC fails

D1 · Everything fails as SERVFAIL

The analogy. Think of a teller saying "I cannot process this" and nothing more.

No explanation of which part looked wrong. That is deliberate — a detailed explanation could itself be forged, and telling a forger exactly which detail to fix is not security.

So you get one flat refusal for every possible cause, and one question separates them: "and if you skip the signature check, does it go through?"

A validation failure produces SERVFAIL and nothing else. No error text, no indication that cryptography was involved, no hint about which record or which key.

This is deliberate: the resolver must not hand the client data it could not authenticate, and a detailed error would itself be unauthenticated. But operationally it means DNSSEC failures are indistinguishable from network failures at first glance — which is why Module 01 D3 could only say "SERVFAIL is a prompt, not a diagnosis".

The one command that separates them is +cd. If the name fails normally and resolves with +cd, the data is reachable and validation is what is failing. That single option turns an unbounded search into a two-line diagnosis.

🧪 Exercise D1.1 — Tamper with a signed zone and watch it go bogus
bash
cd /tmp/lab
# change ONE address in the SIGNED file, without re-signing.
# the RRSIG no longer matches the data it covers.
sed -i '22s/192.0.2.99/198.51.100.1/' p/zone.db.signed
sed -i 's/2026081704 ; serial/2026081706 ; serial/' p/zone.db.signed
rndc -c /tmp/lab/rndc.conf reload lab.internal
sleep 2
# restart the resolver so it is not answering from cache
pkill -f 'named -c /tmp/lab/r/named.conf'
setsid named -c /tmp/lab/r/named.conf > /tmp/lab/r.log 2>&1 < /dev/null ; sleep 3

echo "--- authoritative server, no validation ---"
dig @127.0.0.1 -p 5301 lab.internal A +short
echo "--- through the VALIDATING resolver ---"
dig @127.0.0.1 -p 5303 lab.internal A +noall +comments | head -3
echo "--- same resolver, validation disabled ---"
dig @127.0.0.1 -p 5303 lab.internal A +cd +short
Expected result — click to reveal
plain text
--- authoritative server, no validation ---
198.51.100.1

--- through the VALIDATING resolver ---
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 53643
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

--- same resolver, validation disabled ---
198.51.100.1

This is the diagnostic pattern, and it is worth memorising as a shape rather than a fact.

Line 1 — the authoritative server hands over the tampered record happily. It does not validate its own data; it just serves the file. So "the authoritative server answers fine" tells you nothing about DNSSEC.

Line 2 — SERVFAIL, and note ad is gone. The resolver checked the signature against the data, found they disagreed, and refused to return anything at all. It did not return the record with a warning. DNSSEC fails closed.

Line 3 — +cd returns 198.51.100.1. The data was there the whole time; only validation was blocking it. A name that SERVFAILs normally and resolves with +cd is a DNSSEC failure, full stop.

And observe what a real attack would look like from the user's side: SERVFAIL. Not a wrong address. DNSSEC's protection is that you get nothing rather than something forged — which is why an over-eager "fix" of disabling validation to make an outage go away is exactly the wrong instinct.

Restore the lab: re-run dnssec-signzone, bump the serial, rndc reload.


D2 · The four ways it actually breaks

The analogy. Think of the most common reason a business stops trading: not a disaster, a licence that quietly expired.

Nobody did anything wrong. There is no incident to review, no change to roll back, and no entry in any log. A date passed, and that was enough.

That is the top row of the table below, and it outnumbers the other three combined.

FailureFrequencyWhat happened
Expired signaturesMost commonNobody re-signed. Signatures expire on a date, not on a change. "We didn't change anything" is the symptom
DS / DNSKEY mismatchCommonKeys rolled without updating the DS at the registrar, or the DS published before the signed zone was live
Broken transportCommonSigned responses exceed 1232 bytes and TCP/53 is blocked — Module 04 B2, now with real consequences
Clock skewOccasionalA validator whose clock is wrong sees valid signatures as expired or not-yet-valid
The rollover rule that prevents most of the second row: overlap, always.

For a ZSK roll: publish the new key alongside the old, wait for the DNSKEY RRset's TTL to expire everywhere, sign with the new key, wait again, then withdraw the old key. At no point is a signature in the wild referring to a key that is no longer published.

For a KSK roll: same principle, but the waiting period is governed by the parent's DS TTL, which you do not control. Publish the new KSK, get the new DS into the parent, wait out the parent's TTL, then stop using the old key.

Every DNSSEC outage caused by a rollover is the same mistake: removing something before every cache that might hold it has expired. Modern BIND's dnssec-policy automates the timing precisely so humans do not have to get it right at 2am.


D3 · Field recipe — is this SERVFAIL a DNSSEC problem?

The analogy. Think of one question that separates "our system is down" from "your signature did not match".

Both get you turned away at the counter with the same words. Asking the teller to skip the signature check is the one question that tells them apart — and it takes a second.

bash
NAME=example.org
R=1.1.1.1

echo "1. normal query"        ; dig @$R $NAME A +noall +comments | grep status
echo "2. validation disabled" ; dig @$R $NAME A +cd +noall +comments | grep status
echo "3. what the signatures say"
dig $NAME A +dnssec +noall +answer | awk '/RRSIG/{print "expires:",$9," inception:",$10}'
echo "4. does the parent have a DS, and does it match the DNSKEY?"
dig $NAME DS    +noall +answer
dig $NAME DNSKEY +noall +answer | head -2
echo "5. full validation trace"
delv @$R $NAME A +rtrace 2>&1 | tail -5
echo "6. is TCP available? signed answers are big"
dig @$R $NAME DNSKEY +dnssec +tcp +noall +stats | grep -E 'MSG SIZE|SERVER'
Read steps 1 and 2 as a pair — that is the whole triage.

SERVFAIL then NOERROR → a DNSSEC validation failure. Go to steps 3 and 4.

SERVFAIL then SERVFAIL → not DNSSEC. It is a reachability or delegation problem; go back to Module 03 D1.

Step 3 is where most real incidents end, because the expiration date has passed. Step 6 catches the other common one: signed responses are large, and a firewall that blocks TCP/53 makes DNSSEC intermittently unusable while plain lookups keep working.

🎯 Interview questions — DNSSEC failures

Q. A DNSSEC-signed domain suddenly stops resolving. Walk me through it.

First I confirm it is DNSSEC at all: query normally, then with +cd. SERVFAIL that becomes NOERROR with +cd means validation is failing and the data itself is fine. If both fail, it is not DNSSEC and I go back to reachability and delegation.

Then the four causes in order of likelihood: expired RRSIGs — check the expiration field, and this is the most common by a distance; a DS/DNSKEY mismatch after a key roll; blocked TCP/53, since signed responses often exceed the UDP buffer; and clock skew on the validator.

The point I would make explicitly, because it is counter-intuitive: "we didn't change anything" is not exculpatory, it is the diagnosis. Signatures expire on a wall-clock date whether or not the zone changes, so a zone nobody has touched for a month is exactly the zone that breaks. Re-signing is a scheduled job, not a step in a change process — which is why dnssec-policy exists.

Q. Why do DNSSEC failures always look like SERVFAIL?

Because a validating resolver that cannot authenticate an answer must not return it, and it has no authenticated way to explain why — a detailed error would itself be forgeable. So it returns the generic server-failure code.

It means DNSSEC failures are indistinguishable from network failures on first inspection, which is why +cd is the essential tool: it isolates validation from everything else in one query.

The design point worth adding: this is DNSSEC failing closed, and that is the intended behaviour. Under attack, the user gets nothing rather than something forged. It also explains why the instinctive fix — turning off validation to make the outage stop — is precisely backwards, and why a signed zone raises your operational stakes as well as your security.


Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    KSK["KSK - flags 257<br>signs ONLY the DNSKEY RRset<br>rotated rarely"] --> DNSKEY["DNSKEY RRset<br>published in YOUR zone"]
    ZSK["ZSK - flags 256<br>signs every other RRset<br>rotated freely"] --> DNSKEY
    KSK -->|"hash of"| DS["DS record<br>lives in the PARENT<br>published via your REGISTRAR"]
    ZSK -->|"produces"| RRSIG["RRSIG per RRset<br>EXPIRES ON A DATE<br>whether or not you change anything"]
    NSEC["NSEC3<br>signs the GAPS<br>proves non-existence<br>enables zone walking"] --> RRSIG
    DS --> V{"validating resolver"}
    DNSKEY --> V
    RRSIG --> V
    V -->|"signature matches,<br>chain reaches the root"| OK["answer returned<br>ad flag set"]
    V -->|"anything wrong"| BAD["SERVFAIL<br>no data, no explanation<br>+cd is the only way in"]
    style DS fill:#f59e0b,color:#fff
    style RRSIG fill:#ef4444,color:#fff
    style OK fill:#22c55e,color:#fff
    style BAD fill:#ef4444,color:#fff

Four ideas, and the rest is detail.

  1. The DS lives in the parent, everything else is yours. That split is why key rollovers need registrar coordination and careful ordering.
  2. Signatures expire on a date, not on a change. The most common DNSSEC outage happens to zones nobody touched.
  3. Failure is total and silent: SERVFAIL, no explanation. +cd is the only tool that separates it from a network fault.
  4. Signing roughly triples response size, so DNSSEC turns Module 04's blocked-TCP problem from theoretical into an outage.

E2 · Production practice

HabitWhy
Use dnssec-policy rather than hand-run dnssec-signzoneIt re-signs and rolls keys on a schedule. Expiry is a clock problem, and clocks should be owned by machines
Alert on RRSIG expiry days in advance, not on failureBy the time validation fails, your domain is already dark for every validating resolver
Confirm TCP/53 works end to end before signingSigning triples response sizes; a firewall allowing only UDP/53 turns signing into an intermittent outage
Algorithm 13 (ECDSAP256SHA256), not RSARoughly a quarter the signature size, which keeps ordinary responses under the 1232-byte buffer
Publish the DS after the signed zone is live everywhereDS first means resolvers demand signatures your servers are not yet serving. Instant SERVFAIL
Remove the DS before un-signing, and wait out the parent TTLThe reverse order takes the domain dark for the length of a TTL you do not control
Overlap keys on every rolloverEvery rollover outage is the same mistake: withdrawing something before all caches holding it expired
Keep .private key files out of Git and out of imagesThey sign anything for your zone. On serious deployments they belong in an HSM
Keep NTP healthy on validators and signersSignature validity is wall-clock. A skewed clock rejects perfectly good signatures
NSEC3 with 0 iterations and no salt, per RFC 9276The iterations cost your servers far more than the attacker, and never made enumeration infeasible
Never "fix" a DNSSEC outage by disabling validationFailing closed is the feature. Disabling it hides an unauthenticated answer you were being protected from

E3 · Capstone exercise

Sign, validate, break, diagnose, repair. No scrolling back.

Brief. On the lab.internal zone from Module 05, do all eight:

  1. Sign the zone with two keys and say which key signs what, from the output rather than from memory.
  2. Produce the DS and say exactly who publishes it and where.
  3. Configure a validating resolver and show the single piece of evidence that validation happened.
  4. Break the zone so that it is cryptographically invalid without changing any key, and show the resulting status.
  5. Prove in one further command that the failure is DNSSEC rather than the network.
  6. Explain why the authoritative server still answers the tampered query happily.
  7. Give the correct ordering for a KSK rollover, and name the TTL that sets its pace.
  8. Name the most common cause of DNSSEC outages in production and say why it is invisible to change management.
Model answer — attempt it first, then click

1. dnssec-keygen -f KSK produces flags 257, plain dnssec-keygen produces 256. From the signed zone: every RRSIG over ordinary RRsets carries the ZSK's key tag (63231 here), while the RRSIG over the DNSKEY RRset carries the KSK's (50540). Read the key tags in the RRSIGs — that is the evidence, not the man page.

2. cat p/dsset-lab.internal.lab.internal. IN DS 50540 13 2 E2DF05... — a hash of the KSK. You generate it; your parent publishes it; you get it there through your registrar. It never appears in your own zone file. lab.internal has no real parent, which is why the lab uses a static trust anchor instead.

3. The ad flag in the resolver's response. Not the presence of RRSIG records — those appear whenever +dnssec is used, signed or not, and prove nothing. ad means this resolver verified the chain.

4. Edit one record in zone.db.signed and reload without re-signing:

plain text
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
;; flags: qr rd ra;                        <- note: no ad

The RRSIG no longer matches the data it covers. No key was touched — the requirement's constraint — and the zone is still bogus.

5. dig @resolver lab.internal A +cd +short198.51.100.1. SERVFAIL normally, data with +cd, therefore validation is the failure. One command, and the network is eliminated.

6. Because an authoritative server does not validate its own data — it serves the file it was given. Signatures are checked by resolvers, not by the server that publishes them. This is why "the nameserver is up and answering" is worthless evidence in a DNSSEC incident, and why you must test through a validating resolver.

7. KSK rollover, in order: generate the new KSK → publish it in the DNSKEY RRset alongside the old → sign the DNSKEY RRset with both → submit the new DS to the registrar → wait out the parent's DS TTL → stop signing with the old KSK → remove it.

The pace is set by the parent's DS TTL — a value in someone else's zone that you do not control, and typically 24–48 hours at a TLD. That is the entire reason KSK rolls are rare and ZSK rolls are routine.

8. Expired signatures. RRSIG records carry a wall-clock expiration — 30 days by default — and expire whether or not anything changes.

It is invisible to change management because there is no change. No deployment, no ticket, no diff. A zone nobody has touched for a month is precisely the zone that goes dark, and every after-the-fact review finds an empty change log. The fix is not process, it is automation: dnssec-policy re-signing on a schedule, plus monitoring that alerts on the expiry date approaching, not on validation failing.

The five things most people miss:

  1. Reading key tags in requirement 1 rather than asserting which key does what
  2. Requirement 3 — that RRSIG presence is not evidence. +dnssec shows signatures whether or not anyone validated them; only ad means validation occurred
  3. Requirement 6 — that authoritative servers never validate. It reframes the entire debugging approach
  4. Naming the parent's DS TTL in requirement 7. "Wait a bit" is not an answer; the number belongs to someone else
  5. Requirement 8's second half. Everyone names expiry; far fewer explain why change management cannot catch it

E4 · Official documentation

LinkCovers
RFC 9364 — DNS Security Extensions (BCP 237)The current top-level statement of what DNSSEC is. Start here, not with RFC 4033
RFC 4033 — Introduction and Requirements · RFC 4034 — Resource Records · RFC 4035 — Protocol ModificationsThe founding trio. 4034 is the one to keep open — it defines DNSKEY, RRSIG, DS and NSEC
RFC 6840 — Clarifications and Implementation NotesCorrections to the trio. Read before implementing anything
RFC 6781 — DNSSEC Operational Practices v2Key rollovers, timing, overlap. The operational half of this module
RFC 5155 — NSEC3 · RFC 9276 — NSEC3 Parameter Guidance · RFC 7129 — Authenticated Denial of ExistenceProving non-existence, and why the modern recommendation is zero iterations
RFC 8624 — Algorithm Implementation RequirementsWhich algorithms to use and which to retire
BIND 9 DNSSEC Guide · DNSSEC chapterThe practical walkthrough, including dnssec-policy. The best DNSSEC tutorial there is
dnssec-keygen, dnssec-signzone, dnssec-dsfromkey, delvEvery tool used in Part C
RFC 5910 — DNSSEC mapping for EPPHow a DS actually reaches the parent zone through your registrar
How to read these efficiently.

Read RFC 9364 first — it is short and current, and it replaces the job RFC 4033 used to do as an introduction.

Then read the BIND 9 DNSSEC Guide end to end. It is unusually good: a worked example rather than a specification, and it covers dnssec-policy, which is what you will actually deploy.

Keep RFC 4034 open as a record reference and skip the rest of the trio unless you are implementing a validator.

Read RFC 6781 §4 before your first key rollover, not during it.

The offline route. man dnssec-signzone documents every flag used in C2, and delv +rtrace is a self-documenting validation debugger — it prints each fetch and where the chain breaks, which is more useful during an incident than any specification.


E5 · Self-assessment

1. What does DNSSEC guarantee, and what does it explicitly not?

It guarantees authenticity and integrity: the data came from the zone's key holder and was not modified in transit.

It does not provide confidentiality — queries and answers remain plaintext. It does not protect the last hop from your validating resolver to your device, where only the ad bit exists. And it says nothing about trustworthiness — a phishing domain can be perfectly signed.

2. Why a KSK and a ZSK?

Because the parent's DS points at the KSK, so changing the KSK needs a registrar transaction and a wait on the parent's TTL. The ZSK is referenced only inside your zone and can be rolled freely.

The KSK signs only the DNSKEY RRset; the ZSK signs everything else.

It is a separation of operational cadence, not of cryptographic strength.

3. Where does the DS record live, and why does that matter?

In the parent zone, published through your registrar — never in your own zone file.

It matters because it is a piece of your security configuration in someone else's system, on someone else's change cadence and TTL. Exactly the same class of problem as glue records.

It also dictates ordering: publish the DS after the signed zone is live, and remove it before un-signing.

4. What is the most common cause of DNSSEC outages?

Expired RRSIGs. Signatures carry a wall-clock expiration — 30 days by default — and expire whether or not the zone changes.

It is invisible to change management because there is no change to review. Re-signing must be a scheduled job, which is what dnssec-policy exists to own.

5. How do you tell a DNSSEC failure from a network failure?

Query normally, then with +cd. SERVFAIL that becomes NOERROR with +cd means the data is reachable and validation is what is failing.

If both fail, it is not DNSSEC — go back to reachability, delegation and the transport checks.

6. Why does a validation failure return SERVFAIL with no explanation?

Because the resolver must not return data it could not authenticate, and any detailed error would itself be unauthenticated and therefore forgeable.

DNSSEC fails closed by design: under attack you get nothing rather than something forged. It is also why disabling validation to end an outage is exactly the wrong instinct.

7. Why does an authoritative server serve tampered signed data without complaint?

Because authoritative servers do not validate. They serve the zone file they were given; signature checking is a resolver function.

So "the nameserver is up and answering" is worthless evidence in a DNSSEC incident. You must test through a validating resolver.

8. How does DNSSEC prove non-existence, and what does that cost you?

NSEC/NSEC3 sign the gaps between sorted names, so a resolver can verify that nothing exists between two neighbours.

The cost is zone enumeration: plain NSEC lets anyone walk the chain and list every name. NSEC3 hashes the names, raising the cost but not removing it, and RFC 9276 now recommends zero extra iterations because they burdened your servers more than the attacker.

If names must be secret, use split-horizon rather than relying on NSEC3.

9. What must you check about transport before signing a zone?

That TCP/53 works end to end. Signing roughly triples response sizes, and with the modern 1232-byte EDNS buffer many signed responses truncate and require a TCP retry.

If a firewall allows only UDP/53, signing converts a working domain into an intermittently broken one — small queries fine, signed ones hanging.

Choosing algorithm 13 rather than RSA keeps signatures about a quarter the size and reduces how often this matters.

10. Give the ordering rule that prevents rollover outages.

Always overlap, and never withdraw anything until every cache that could hold it has expired.

Publish the new key alongside the old, wait for the relevant TTL, switch signing, wait again, then remove the old key. For a KSK the pace is set by the parent's DS TTL, which you do not control.

Every rollover outage is the same mistake: removing something too early.


Next — Module 08 · DNS in the Cloud & in Kubernetes.

Everything so far has been DNS as a protocol. Module 08 is DNS as most DevOps engineers actually meet it: Route 53 alias records, routing policies and private hosted zones; split-horizon done properly; then CoreDNS, the Corefile, cluster service names, headless services, and the ndots:5 search list that turns Module 06 B3's arithmetic into the notorious five-second Kubernetes timeout.

📚 Sources for the interview questions

Every key, signature, DS record, validated response and SERVFAIL in this module was produced for real on 20 August 2026: lab.internal signed with dnssec-keygen/dnssec-signzone 9.18 using ECDSAP256SHA256, served by a BIND primary on port 5301, and validated by a second BIND instance on port 5303 configured with the KSK as a static trust anchor. The tampering in D1.1 and the resulting SERVFAIL / +cd pair are captured output.

Specifications verified directly: RFC 4033, RFC 4034, RFC 4035, RFC 5155, RFC 5910, RFC 6781, RFC 6840, RFC 7129, RFC 8624, RFC 9276, RFC 9364, plus the BIND 9 DNSSEC Guide.

Question selection cross-referenced against publicly published 2026 DNS and networking interview question sets:

Answers were rewritten and deepened rather than reproduced. Published sets describe the chain of trust and stop. The parts that actually come up once an interviewer has operated a signed zone — that signatures expire on a date rather than on a change, that authoritative servers never validate their own data, that every failure looks identical, and that the DS lives in someone else's system — appear in almost none of them.

Spotted a mistake or want something added? Send me a note.