is it dns? / wiki ← back to the live monitor

DNSSEC troubleshooting: keeping out of BOGUS

When DNSSEC works, you notice nothing: answers come back validated and everything is fine. When DNSSEC breaks, every lookup for the affected zone returns SERVFAIL and there is no graceful degradation. This page is the field guide for what breaks, how to tell, and how to stay out of trouble.

Why DNSSEC failures are total#

DNSSEC is intentionally hard-fail. There is no "soft" failure mode: a validating resolver either has a clean signature chain to the root, or it returns SERVFAIL, on modern resolvers with an extended-error code (EDE) indicating why. The design choice: DNSSEC's job is to refuse to lie, and a soft-fail mode would defeat the purpose by letting an attacker who can strip signatures convince validators to fall back to unsigned answers.

So when DNSSEC breaks, your zone goes dark for every validator on the Internet for the duration of their cached SERVFAIL. SERVFAIL caching is controlled by the resolver operator (RFC 9520 requires resolvers to cache resolution failures), not signaled by your zone. Unbound caches DNSSEC-bogus results for 60 seconds by default via val-bogus-ttl (unbound.conf(5)); BIND's servfail-ttl defaults to 1 second (max 30; higher values are silently reduced, per the BIND ARM). You cannot shorten a SERVFAIL storm by editing your SOA. What you can do: fix the chain and wait for cached SERVFAIL responses to expire (up to five minutes per RFC 9520). The SOA minimum field governs NXDOMAIN caching (RFC 2308), not SERVFAIL; lowering it before a migration still helps if you will be pulling names that previously existed, but it does nothing for DNSSEC-failure cache duration.

What "becoming BOGUS" means#

A zone is BOGUS (or "in a bogus state") when a validating resolver cannot verify the chain of signatures from the root down to the answer. The resolver returns SERVFAIL, often with an Extended DNS Error (EDE) code (RFC 8914) that tells you which step failed. See dnssec for how the trust chain is constructed in the first place.

EDE codeNameMeans
EDE 0Other ErrorGeneric: keep digging
EDE 1Unsupported DNSKEY AlgorithmResolver doesn't implement the zone's signing algorithm
EDE 2Unsupported DS Digest TypeThe DS at the parent uses a digest type the resolver doesn't implement
EDE 4Forged AnswerResolver deliberately returned a non-authentic answer for policy reasons (e.g. a filtered or blocked name); used only when an answer is still provided rather than a failure rcode, so it is not a validation-failure code
EDE 5DNSSEC IndeterminateResolver can't determine validity
EDE 6DNSSEC BogusValidation failed
EDE 7Signature ExpiredRRSIG past expiration
EDE 8Signature Not Yet ValidRRSIG inception in the future
EDE 9DNSKEY MissingCan't find the signing key
EDE 10RRSIGs MissingRecords aren't signed
EDE 12NSEC MissingCan't prove non-existence

EDE rides in the EDNS OPT record, so any query that carries EDNS can carry it back: dig sends EDNS by default, and both 1.1.1.1 and 8.8.8.8 attach the code without needing +dnssec. You need dig from BIND 9.16.4 or newer to see it printed (as an EDE: line in the OPT pseudosection; change 5408 added it). Cloudflare even keeps the EDE on +cd responses, right alongside the unvalidated answer. The full IANA registry of EDE values lives at iana.org/assignments/dns-parameters.

A permanent BOGUS canary: dnssec-failed.org#

This zone exists for one purpose: it is deliberately broken. The operators publish DNSKEY records whose hashes do not match the DS record at the parent, so any validating resolver should refuse to answer. The expected response is SERVFAIL with EDE 9 (DNSKEY Missing): the resolver found the DS at the parent but could not find a matching DNSKEY in the zone.

The SERVFAIL arrives with or without the +dnssec flag from a validating resolver. An actual address means the validator is not in your path.

dig @1.1.1.1 +dnssec dnssec-failed.org A
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 58290
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for dnssec-failed.org.)

nslookup can't show EDE codes or the DO bit, but it does surface the SERVFAIL clearly:

nslookup dnssec-failed.org 1.1.1.1
Server:		1.1.1.1
Address:	1.1.1.1#53

** server can't find dnssec-failed.org: SERVFAIL

Confirm the EDE reason code via DoH (no local resolver interference):

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=dnssec-failed.org&type=A'
{
  "Status": 2,
  "AD": false,
  "Comment": ["EDE(9): DNSKEY Missing no SEP matching the DS found for dnssec-failed.org."]
}

To see what the zone actually contains, bypassing validation with +cd (Checking Disabled):

dig @1.1.1.1 +dnssec dnssec-failed.org A +cd
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63696
;; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for dnssec-failed.org.)

;; ANSWER SECTION:
dnssec-failed.org.  300  IN  A      96.99.227.255
dnssec-failed.org.  300  IN  RRSIG  A 13 2 300 20260723015706 20260715225206 32784 dnssec-failed.org. zYXpmdcfbSRa7KYCaHbxSSnurpYEHZjGS0/NuZ8Q0EeVtD0nicxi/EIK e6jKlyJmdWOQs5BPYQit+hjEMPJskA==

The RRSIG inception/expiration timestamps and the signature itself change every time the zone re-signs; expect different values on your run. Note that Cloudflare still attaches the EDE 9 diagnostic even though +cd let the answer through.

nslookup cannot set the CD (Checking Disabled) bit. Use dig for this test.

If your resolver returns NOERROR on the first query (without +cd), DNSSEC validation is off or broken on the resolver. That is the test. Some operators keep a continuous external probe pointed at dnssec-failed.org: if the canary ever returns NOERROR through their validator, an alert fires before users notice.

Other handy canaries:

A validating resolver sets the ad bit on answers you queried with +dnssec. Its absence is not proof a zone is unsigned: on-path interceptors and some routers strip ad before it reaches a stub. delv and RRSIG presence are the path-independent checks.

ZoneExpected behavior
dnssec-failed.orgSERVFAIL with EDE 9 (DNSKEY Missing, by design); delv reports resolution failure
cloudflare.comNOERROR; delv reports "fully validated"
ietf.orgNOERROR; delv reports "fully validated"
iana.orgNOERROR; delv reports "fully validated"
isitdns.netNOERROR with AD=true; delv reports "fully validated" (DS keytag 2371 at .net)

How a zone becomes bogus#

The most common ways, in roughly the order you'll encounter them:

1. Signatures expired#

RRSIG records have an explicit expiration (typically 30 days from issuance). Authoritative servers resign before expiry. If signing stops (a forgotten cron, a permissions break, an HSM offline), signatures expire and the zone goes bogus.

Tell: EDE 7 (Signature Expired). Fix: re-sign immediately.

2. Key rollover without parent DS update#

A "KSK rollover" replaces the zone's Key Signing Key. The new KSK's hash must be published at the parent zone as a DS record (see dnssec for how DS records cross zone boundaries). If the rollover happens but the DS is not updated, validators chase the chain to an unknown key and refuse.

Tell: EDE 9 (DNSKEY Missing) or EDE 5 (Indeterminate). Fix: publish the new DS at the parent and wait for TTL.

3. Zone moved between providers with DNSSEC active#

Migrating a zone from one provider to another while DNSSEC is on at both ends is a guaranteed outage. The DS at the registrar still hashes the old KSK while the new provider's KSK has different bits.

Always disable DNSSEC at the old provider before changing nameservers, then re-enable at the new provider once the new DS is published. This is the #1 way people break DNSSEC.

4. Algorithm mismatch#

You roll a key to a new algorithm (RSA → ECDSA) but the DS at the parent still references the old algorithm. Validators see a chain that doesn't line up.

Tell: EDE 6 (Bogus) or EDE 1 (Unsupported DNSKEY Algorithm).

5. NSEC / NSEC3 inconsistency#

NSEC records prove non-existence. If a zone has DNSSEC but the NSEC chain is incomplete or inconsistent (a record was added without re-signing the NSEC), validators reject the response.

Tell: EDE 10 (RRSIGs Missing) or EDE 12 (NSEC Missing). Fix: re-sign.

6. Clock skew on the resolver or authoritative#

RRSIG validity is bounded by absolute timestamps. If the resolver's clock is wildly off, signatures look "not yet valid" or "expired" even when they aren't.

Tell: EDE 7 or EDE 8. Fix: ensure NTP is running on every resolver and every authoritative server in the chain. Drift on either end produces the same symptom.

Triage flow#

Symptom: dig @1.1.1.1 <name> → SERVFAIL
   │
   ├──▶ Try with +cd → does it succeed?
   │       │
   │       ├── YES → DNSSEC is the cause. Continue ↓
   │       └── NO  → Not DNSSEC. Look at recursion/forwarders.
   │
   ├──▶ dig @1.1.1.1 +dnssec <name> → look at EDE in the response
   │       │                            (needs dig from BIND 9.16.4+; curl DoH always works)
   │       ├── EDE 7  → expired RRSIG. Re-sign.
   │       ├── EDE 9  → missing DNSKEY. Check zone + parent DS.
   │       ├── EDE 10 → missing RRSIGs. Zone has DS but is not actually signed.
   │       └── EDE 6  → DNSSEC Bogus. Run DNSViz.
   │
   ├──▶ Visualize the whole chain in DNSViz
   │
   └──▶ Compare against a known-signed zone with delv:
           delv @<your-resolver> ietf.org A
           "fully validated"  → problem is zone-specific
           "failure"          → resolver DNSSEC pipeline has a broader problem

The DNSViz walkthrough lives on its own page: DNSViz: visualize the whole signature chain.

For the first step, nslookup gives you the same yes/no on SERVFAIL, just without the EDE code:

nslookup <name> 1.1.1.1
** server can't find <name>: SERVFAIL

When you need the EDE code and dig is too old to show it, use curl DoH:

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=<name>&type=A'

The Comment field in the JSON response carries the EDE string verbatim.

Quick delv validation#

delv ships with the BIND utilities package. Unlike dig +dnssec, which shows raw DNSSEC records and lets you read them yourself, delv walks the chain on your behalf, validates each link against its own embedded trust anchor for the root, and prints a single-line verdict:

macOS note: the delv bundled with macOS (BIND 9.10.6) was compiled without crypto support and prints no crypto support instead of validation results. Install a current version with brew install bind to get a working delv.

nslookup cannot perform DNSSEC chain validation. Use delv for these checks.

delv @1.1.1.1 cloudflare.com A
; fully validated
cloudflare.com.  300 IN A  104.16.132.229
cloudflare.com.  300 IN A  104.16.133.229
cloudflare.com.  300 IN RRSIG A 13 2 300 ...
delv @1.1.1.1 +rtrace cloudflare.com A
;; fetch: cloudflare.com/A
;; fetch: cloudflare.com/DNSKEY
;; fetch: cloudflare.com/DS
;; fetch: com/DNSKEY
;; fetch: com/DS
;; fetch: ./DNSKEY
; fully validated
...

The +rtrace flag logs every fetch delv makes internally: DNSKEY lookups, DS checks, and the final record. Useful when you need to see exactly where the chain diverges. Two related flags per the delv manual: +vtrace shows the validator's step-by-step reasoning, and +mtrace dumps the full response messages.

delv @1.1.1.1 dnssec-failed.org A
;; resolution failed: failure
delv @1.1.1.1 neverssl.com A
; unsigned answer
neverssl.com.  60 IN A  34.223.124.45

Because delv carries its own trust anchor, it tells you whether a correctly behaving validating resolver would accept the chain, even if the resolver you are testing through is itself misconfigured.

Confirm isitdns.net as a live working example of a fully anchored zone:

delv @1.1.1.1 isitdns.net A
; fully validated
isitdns.net.  300 IN A  104.21.56.211
isitdns.net.  300 IN A  172.67.155.242
isitdns.net.  300 IN RRSIG A 13 2 300 ...

Staying out of BOGUS: operational checklist#

  1. Automate signing. Whoever signs your zone needs a process that will not quietly stop. A managed DNS provider takes this off your plate. If you sign yourself, the signing job has to be a monitored scheduled task, not "I remember to re-sign before the 30-day window closes."
  1. Audit that the DS at the registrar matches your zone's KSK. Query the parent zone directly:
   dig @a.gtld-servers.net isitdns.net DS
   ;; ANSWER SECTION:
   isitdns.net.  86400  IN  DS  2371 13 2 2DF8E1D4E0855DF60F2A8B51C914F2F2866ED1E3344F74F9682DAC4D42D18802

Then confirm the same keytag appears in your zone's DNSKEY (+multiline makes dig compute and print the key id):

   dig @1.1.1.1 isitdns.net DNSKEY +multiline
   isitdns.net.  3600 IN DNSKEY 257 3 13 (
           ...
           ) ; KSK; alg = ECDSAP256SHA256 ; key id = 2371

Run this after any provider change, after any rollover, and on a regular schedule. If the keytag in the DS does not match any DNSKEY in the zone, validators will return SERVFAIL with EDE 9.

  1. NTP everywhere. Clock skew breaks RRSIG validity windows. Run NTP on every resolver and every authoritative server in the chain; drift on either end produces the same symptom.
  1. Keep a canary probe running. dnssec-failed.org returning SERVFAIL is a continuous "validation is on" heartbeat. If your monitoring ever sees NOERROR from that zone via your validator, validation has silently broken somewhere.
  1. Test before migrating. Any DNSSEC-relevant change (key rollover, algorithm change, provider migration) should be probed against a side zone first. Do not experiment on the production zone.
  1. Lower the negative-cache TTL before a migration. Only useful if you will be removing previously-existing names: the SOA minimum field controls NXDOMAIN caching (RFC 2308), not SERVFAIL. SERVFAIL caching is configured on the resolver (capped at five minutes per RFC 9520) and you cannot shorten it from your zone.

See also#