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

DNSSEC, signed and validated

DNSSEC means a resolver can prove a response actually came from the zone's owner and wasn't tampered with in flight. It does not mean DNS is encrypted (that is a separate concern: DoT/DoH/DoQ) and it does not mean the records are private. It means authenticity.

The trust chain#

.                root, signed; key trust comes from your resolver shipping the root KSK
net.             TLD, signed by root via DS (Delegation Signer)
isitdns.net.     zone, signed by .net via DS
www.isitdns.net. record, signed by zone ZSK

Every link is a DS (the parent's hash of the child's KSK) plus a DNSKEY (the child's actual key). A resolver follows DS → DNSKEY → RRSIG (the signature over the record set) at each level and either gets to the root via a chain it trusts, or fails. See anatomy-of-a-query for how the resolver walks this chain hop by hop.

Is a zone signed?#

A two-step check anyone can run. isitdns.net is fully signed and anchored, so these commands return real values:

dig +short DNSKEY isitdns.net @1.1.1.1
256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8 KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==
257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+ KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
nslookup -type=DNSKEY isitdns.net 1.1.1.1
isitdns.net	rdata_48 = 256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8 KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==
isitdns.net	rdata_48 = 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+ KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==

nslookup has no pretty-printer for the DNSKEY type, so it dumps the raw rdata as rdata_48 (48 is DNSKEY's type code; even current BIND 9.18 nslookup does this). The key material is identical; dig just labels it more clearly. The space inside the key is how both tools split long fields into 56-character chunks for display; it is not part of the data.

Now check that the parent (.net TLD) has published a DS that fingerprints the zone's KSK:

dig +short DS isitdns.net @a.gtld-servers.net
2371 13 2 2DF8E1D4E0855DF60F2A8B51C914F2F2866ED1E3344F74F9682DAC4D 42D18802
nslookup -type=DS isitdns.net 1.1.1.1
isitdns.net	rdata_43 = 2371 13 2 2DF8E1D4E0855DF60F2A8B51C914F2F2866ED1E3344F74F9682DAC4D 42D18802

If both queries return values, the zone is signed and the trust chain to the parent is in place. If either is empty, the zone is unsigned (or the chain is broken). Use dnssec-troubleshooting to diagnose which link is missing.

See it in action#

delv is the reliable cross-resolver validation tool (ships with the BIND utilities package). It carries its own root trust anchor and walks the chain itself, so it reports a definitive verdict regardless of whether the ad bit survives the path to your stub.

macOS caveat: the delv that ships with macOS (BIND 9.10.6) was built without crypto support and returns no crypto support instead of a validation verdict. Install the full BIND package first: brew install bind. Then use /opt/homebrew/bin/delv (or ensure the Homebrew bin is first on your PATH).

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

If the chain breaks, delv says so. delv @1.1.1.1 dnssec-failed.org A returns:

;; broken trust chain resolving 'dnssec-failed.org/A/IN': 1.1.1.1#53
;; resolution failed: broken trust chain

nslookup cannot show chain validation or the ad bit. Use delv or the DoH proof below.

The RRSIG records in the answer are the other concrete signal that a zone is signed. Request them explicitly with +dnssec:

dig +dnssec isitdns.net A @1.1.1.1
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 ...

The presence of an RRSIG confirms the zone is signed. A validating resolver also sets the ad (Authenticated Data) flag on answers it verified; modern dig requests that bit by default, with or without +dnssec. You can verify the ad bit path-independently over DoH, where a transparent interceptor cannot strip it:

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=isitdns.net&type=A'
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,
 "Question":[{"name":"isitdns.net","type":1}],
 "Answer":[{"name":"isitdns.net","type":1,"TTL":300,"data":"104.21.56.211"},
           {"name":"isitdns.net","type":1,"TTL":300,"data":"172.67.155.242"}]}

"AD":true confirms the validating resolver verified the full chain. If ad is missing from a plain dig answer, the path stripped it: transparent DNS interceptors, some captive portals, and certain consumer routers clear the bit between the resolver and your stub. RRSIG presence is a stronger signal (it is data, not a header flag, though a middlebox that answers for the resolver can drop it too), and a delv verdict is computed cryptographically on your own machine, so nothing on the path can fake it.

A broken DNSSEC chain causes a validating resolver to return SERVFAIL rather than hand back an unverified answer. dnssec-failed.org exists specifically to demonstrate this:

dig dnssec-failed.org A @1.1.1.1
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: ...
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for dnssec-failed.org.)

The EDE line is an Extended DNS Error (RFC 8914): modern resolvers attach one to the SERVFAIL saying why validation failed, so you no longer have to guess. Older resolvers return a bare SERVFAIL; diagnose those with dnsviz or dnssec-troubleshooting.

nslookup can query dnssec-failed.org and will also receive SERVFAIL, but it cannot show you the ad bit or tell you why validation failed.

What DNSSEC protects against#

  • Cache poisoning (Kaminsky-style spoofing of recursive caches)
  • On-path tampering (some ISP or hotel network rewriting A records to ad pages)
  • Compromised authoritative servers, if the keys are stored separately (in practice, often co-located, so this protection is weaker)

What DNSSEC does not protect against#

  • The query is still plaintext on the wire. A passive observer sees what you asked. That is what dot-doh addresses.
  • The zone records are public: nothing about DNSSEC encrypts them.
  • A compromise of the zone's signing key invalidates everything until the key rolls.
  • A compromise of the registrar account lets an attacker re-publish a DS for their key, breaking the chain or pointing it at their zone.

Algorithm choice#

Algorithm 13 (ECDSAP256SHA256) is the modern default. RSA (algorithms 8 and 10) is older but still common. Both validate fine; ECDSA produces smaller signatures, which keeps response sizes down and reduces the risk of UDP truncation (see query-types for how message size interacts with transport).

Key rollovers: the place rollouts go wrong#

DNSSEC fails most often during key rollover when the parent DS is not updated in lock-step with the child DNSKEY. Managed DNS providers automate this end-to-end; most large managed-DNS services handle key rotation without operator action. If you ever manage signing yourself:

  1. Roll the ZSK independently and often (no parent coordination needed)
  2. Roll the KSK rarely, and never break the DS to DNSKEY match: publish the new KSK alongside the old one, wait at least the DNSKEY TTL, swap the DS at the registrar, and retire the old key only after the old DS has expired from caches

RFC 7583 (DNSSEC Key Rollover Timing Considerations) defines the timing model. See dnssec-troubleshooting for diagnosing a rollover that went wrong.

Negative responses get signed too#

NSEC and NSEC3 records prove "this name does not exist" cryptographically. Both types are signed, so an on-path attacker cannot spoof NXDOMAIN. The distinction NSEC3 adds is zone-enumeration protection: NSEC owner names are in plaintext, so an attacker can walk the NSEC chain to enumerate every name in the zone (a technique called NSEC walking). NSEC3 hashes the owner names, making walking far harder.

isitdns.net uses NSEC, in Cloudflare's on-the-fly variant called compact denial of existence (RFC 9824). Query a name that does not exist and the resolver returns the NSEC proof inline:

dig +dnssec doesnotexist.isitdns.net @1.1.1.1 | grep -E "NSEC|status"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
doesnotexist.isitdns.net. 1800 IN NSEC  \000.doesnotexist.isitdns.net. RRSIG NSEC TYPE128
doesnotexist.isitdns.net. 1800 IN RRSIG NSEC 13 3 1800 ...

Two compact-denial quirks show up here. The status is NOERROR rather than NXDOMAIN, and the NSEC is synthesized to cover exactly the queried name (the \000 label is its immediate successor), so the zone leaks nothing about its real contents. The NXNAME meta-type (code 128) in the type bitmap is the signal that the name truly does not exist; dig 9.18 and older print it as TYPE128 because they predate the mnemonic.

nslookup can show NSEC data but cannot interpret it or display the validation verdict. Use dig +dnssec or dnsviz for NSEC chain analysis.

iana.org uses NSEC3 instead. The hashed owner names look like this:

dig +dnssec doesnotexist.iana.org @1.1.1.1 | grep NSEC3
0d5cbi611aogl6kk8jjsopfic6dcb42t.iana.org. 3600 IN NSEC3 1 0 0 - 26CS5JG5RASD1SS5VNTJ9PSC7FDVQIEO CNAME RRSIG
0d5cbi611aogl6kk8jjsopfic6dcb42t.iana.org. 3600 IN RRSIG NSEC3 13 3 3600 ...
mvnqhoigoa305s1i78hp6cdv5n7lcutc.iana.org. 3600 IN NSEC3 1 0 0 - NGJOKE6KAKN5BC83M0IAPQVRBAJKQI3M A NS SOA MX TXT AAAA RRSIG DNSKEY NSEC3PARAM CAA
mvnqhoigoa305s1i78hp6cdv5n7lcutc.iana.org. 3600 IN RRSIG NSEC3 13 3 3600 ...

The hashed labels in NSEC3 (0d5cbi6...) replace the plaintext names that plain NSEC would expose. More than one NSEC3 record comes back because the server has to prove several things at once: that the name's closest existing ancestor is real, and that nothing (including a wildcard) covers the queried name. Use dnsviz to visualize the full NSEC/NSEC3 chain for any signed zone.


See also: anatomy-of-a-query · dnssec-troubleshooting · dnsviz · dot-doh · caa-policy · Cloudflare DNSSEC docs