Networking

DNSSEC

A cryptographic paper trail from the root to the record

DNSSEC adds public-key signatures to DNS so a resolver can cryptographically verify that an answer came from the real zone owner and wasn't forged or tampered with in transit, by validating a chain of trust from the root down to the record.

  • AddsAuthentication + integrity
  • Does NOT addEncryption / privacy
  • Record typesRRSIG, DNSKEY, DS, NSEC/NSEC3
  • Trust modelSingle root anchor
  • Validation failureSERVFAIL

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How DNSSEC builds a chain of trust

Plain DNS is the internet's address book, and for its first two decades it had no idea who it was talking to. A resolver asks "what is the IP of example.com?" over UDP and accepts whatever answer arrives first. There's no signature, no return address it can check. That gap is what made the 2008 Kaminsky cache-poisoning attack so devastating: by flooding a resolver with forged answers carrying guessed transaction IDs, an attacker could inject a fake IP for an entire domain and the resolver would cache it, redirecting every user to a malicious server. The DNS transaction ID is only 16 bits — 65,536 values — so a fast attacker can win the race.

DNSSEC (DNS Security Extensions, standardized in RFC 4033–4035 in 2005) closes that gap not by encrypting anything, but by signing everything. Every set of records of the same name and type — a resource record set, or RRset — gets a digital signature stored in a new record type called RRSIG. The signature is made with the zone owner's private key. The matching public key is published in the zone as a DNSKEY record. A resolver fetches the RRset, fetches its RRSIG, fetches the DNSKEY, and checks the math. If it verifies, the answer is authentic and byte-for-byte intact.

But that just moves the question one level up: how does the resolver trust the DNSKEY? Anyone can publish a key. The answer is the chain of trust. The parent zone publishes a DS (Delegation Signer) record — a hash of the child's key — and signs that with its own key. So com vouches for example.com, the root vouches for com, and the root's key is the one thing every validating resolver ships with, hard-coded: the trust anchor. Trust flows down a single chain from one globally-known key.

The precise mechanism: validating one answer

To validate A records for www.example.com, a resolver walks the chain top-down:

  1. Start at the trust anchor. The resolver already trusts the root DNSKEY (its hash is distributed out-of-band by IANA and pinned in the resolver's config).
  2. Verify the delegation to com. Fetch the DS record for com from the root, and the root's RRSIG over it. Check the RRSIG with the root key. Now the DS hash is trusted.
  3. Verify com's key. Fetch com's DNSKEY. Hash it and compare to the trusted DS. Match ⇒ com's key is now trusted.
  4. Repeat for example.com. com publishes a DS for example.com (signed by com's now-trusted key); fetch example.com's DNSKEY; hash-match against the DS.
  5. Verify the data. Finally, check the RRSIG over the A RRset with example.com's trusted key. Verified ⇒ the IP is authentic.

The cryptography is a standard signature scheme. The signed message is the canonical wire-format encoding of the RRset (records sorted, names lowercased) concatenated with the RRSIG's own metadata (type covered, algorithm, labels, original TTL, signature inception and expiration timestamps). The cost is one signature verification per zone cut — so the work is O(d) where d is the number of labels in the name, typically 3–5. A single ECDSA P-256 verification is on the order of tens of microseconds, so validating a full chain is sub-millisecond CPU; the latency that matters is the extra round-trips to fetch DNSKEY and DS records, mitigated by caching.

Two keys are used per zone by convention. The Key Signing Key (KSK) signs only the DNSKEY RRset and is the key the parent's DS points to. The Zone Signing Key (ZSK) signs everything else. This split lets operators rotate the busy ZSK freely without ever asking the parent to update its DS — the slow, manual, error-prone step.

When DNSSEC is worth it (and when it isn't)

  • You operate a TLD or a high-value domain — a bank, a government, a certificate authority. Cache poisoning here harms millions; signing is mandatory in many ccTLD contracts.
  • You depend on DNS-based authentication. Technologies that only work safely under DNSSEC: DANE (pinning TLS certs in DNS via TLSA records), SSHFP (SSH host keys in DNS), and SMIMEA. Without DNSSEC these are forgeable.
  • You run a public resolver. Validating protects every downstream user from poisoned upstreams. Google Public DNS, Cloudflare 1.1.1.1, and Quad9 all validate.

When to skip it: a small site behind a CDN where HTTPS already authenticates the server, the operator has no automated signing pipeline, and an expired RRSIG would cause a worse outage than the cache-poisoning risk it prevents. DNSSEC done badly is more dangerous than no DNSSEC, because the failure mode is a hard outage rather than a silent degradation.

DNSSEC vs other ways to secure DNS

DNSSECDNS-over-HTTPS / TLSHTTPS (TLS cert)DNSCurve
Authenticity of answerYes (signed at the source)Only the channel to the resolverOf the web server, not DNSChannel only
Integrity / tamper detectionEnd-to-end, source-signedHop-to-resolver onlyOf HTTP bodyHop-to-hop
Confidentiality (privacy)NoneYes — encrypts the queryYes (of HTTP)Yes
Protects against a malicious resolverYes — resolver can't forgeNo — you trust the resolvern/aNo
Trust modelOne global root anchorWeb PKI / pinned resolver~150 CAsPer-server keys
Failure modeSERVFAIL → hard outageFall back to plain DNSBrowser warningFall back
Caching of resultSignatures cache with TTLConnection-levelCert cached by browserNone
Deployed at root / TLDsYes (~90% of TLDs)At specific resolversUniversal on webNiche

The crucial distinction: DNSSEC and DoH solve different problems and are complementary. DoH encrypts the question between you and your resolver but tells you nothing about whether the resolver's answer is genuine. DNSSEC proves the answer is genuine end-to-end but broadcasts the question in the clear. The privacy-and-authenticity ideal runs both: DoH for the transport, DNSSEC validation underneath.

What the numbers actually say

  • Response size grows 3–5×. An unsigned A answer is ~60–100 bytes; add an RRSIG (~150 bytes with ECDSA, more with RSA) plus DNSKEY/DS material and signed responses routinely exceed the old 512-byte UDP limit, forcing EDNS0 and occasional TCP fallback.
  • This makes DNS a 100×+ DDoS amplifier. A 60-byte spoofed query can elicit a multi-kilobyte signed ANY response, which is why ANY queries are now largely refused.
  • Validation CPU is cheap. ECDSA P-256 verify ≈ 30–60 µs on a modern core; signing a zone of a million records takes seconds. The real cost is operational, not computational.
  • Adoption is lopsided. The root and ~90% of TLDs are signed, roughly 30–35% of resolvers validate (measured via APNIC's ad-based methodology), but only a low single-digit percentage of second-level .com domains are signed.
  • Outage history is real. Expired or mis-handled signatures have taken whole TLDs offline — the U.S. .gov zone suffered a DNSSEC validation failure in early 2012, and various ccTLDs (including .fr and .se) have gone dark for hours when re-signing automation lapsed.

JavaScript: verifying an RRSIG and walking the chain

The core operation is a public-key verify over the canonical signed data. Here's the verify step using Node's WebCrypto with an ECDSA P-256 (algorithm 13) key, plus a sketch of the chain walk.

import { webcrypto as crypto } from 'node:crypto';

// signedData = canonical RRSIG RDATA (minus the signature) || canonical RRset wire form
async function verifyRRSIG(signedData, signature, publicKeyJwk) {
  const key = await crypto.subtle.importKey(
    'jwk', publicKeyJwk,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false, ['verify']
  );
  return crypto.subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    key, signature, signedData
  );
}

// Walk the chain top-down. `anchorKey` is the trusted root DNSKEY (the trust anchor).
async function validateChain(anchorKey, links) {
  // links: [{ name, dnskey, dnskeyRRSIG, ds, dsRRSIG }, ...] from root → leaf,
  // where `ds` is the parent's DS RRset (with a `.hash`) delegating to this child.
  let trustedKey = anchorKey;            // we start trusting only the root key
  for (const link of links) {
    // 1. the parent's DS RRset must itself be signed by the parent's TRUSTED key —
    //    this is the step that actually anchors the delegation to the chain above.
    const dsOk = await verifyRRSIG(link.ds.signed, link.dsRRSIG, trustedKey);
    if (!dsOk) return fail(link.name, 'parent RRSIG over DS invalid — chain broken');

    // 2. the child's DNSKEY RRset is self-signed by its own KSK
    const keyOk = await verifyRRSIG(link.dnskey.signed, link.dnskeyRRSIG, link.dnskey.jwk);
    if (!keyOk) return fail(link.name, 'DNSKEY self-signature invalid');

    // 3. the now-trusted DS must match a hash of this child DNSKEY — the actual link
    const computed = await sha256(link.dnskey.wire);
    if (!constantTimeEqual(computed, link.ds.hash))
      return fail(link.name, 'DS does not match DNSKEY — broken delegation');

    trustedKey = link.dnskey.jwk;        // child key is now trusted; descend
  }
  return { secure: true, key: trustedKey };
}

function fail(name, why) { return { secure: false, name, why }; }

Two details that bite people. First, the chain is verified strictly top-down: you may only trust a child key after its hash matches a DS that the already-trusted parent signed — never the other way around. Second, comparing the DS hash must be constant-time; a timing leak there is a side-channel on the (public, but still) verification path, and the habit of constant-time comparison is worth keeping everywhere in crypto code.

Python: validating with dnspython

In practice you lean on a library that knows the canonicalization rules. dnspython implements the RRSIG verification; the interesting logic is the chain walk and the failure handling.

import dns.resolver, dns.dnssec, dns.name, dns.rdatatype, hashlib

def validate_rrset(name, rdtype, dnskey_rrset, dnskey_rrsig):
    """Verify one RRset against a trusted DNSKEY set. Raises on failure."""
    # dns.dnssec.validate handles canonical ordering, the SHA-256 hash,
    # inception/expiration window, and the algorithm dispatch (RSA/ECDSA/EdDSA).
    dns.dnssec.validate(dnskey_rrset, dnskey_rrsig,
                        {dns.name.from_text(name): dnskey_rrset})

def ds_matches_dnskey(ds, dnskey, name):
    # DS = digest( owner_name | DNSKEY RDATA ). Algorithm 2 = SHA-256.
    made = dns.dnssec.make_ds(dns.name.from_text(name), dnskey, 'SHA256')
    return made == ds          # equality over the digest is the trust link

def walk_chain(links, trust_anchor_ds):
    """links: list of (zone_name, dnskey_rrset, dnskey_rrsig, ds_from_parent)
       ordered root -> leaf. trust_anchor_ds pins the root key out-of-band."""
    expected_ds = trust_anchor_ds
    for zone, dnskey, rrsig, ds in links:
        ksk = next(k for k in dnskey if k.flags & 0x0001)   # SEP bit = KSK
        if not ds_matches_dnskey(expected_ds, ksk, zone):
            raise ValueError(f"{zone}: DS/DNSKEY mismatch (bogus delegation)")
        validate_rrset(zone, dns.rdatatype.DNSKEY, dnskey, rrsig)  # self-signature
        expected_ds = ds        # the child supplies the DS for the next level
    return "secure"             # the AD (Authenticated Data) bit may now be set

A resolver reports the outcome in three states, and getting them right matters more than the crypto: Secure (chain validated, sets the AD bit), Insecure (the zone has a signed parent but no DS, i.e. an authenticated absence of DNSSEC — legitimately unsigned), and Bogus (signatures present but invalid or expired → return SERVFAIL). Conflating Insecure with Bogus is the classic implementation bug.

Variants and adjacent pieces worth knowing

NSEC vs NSEC3 (authenticated denial). You can't sign a record that doesn't exist, so DNSSEC signs the gaps. NSEC returns the two existing names that alphabetically bracket the queried-but-missing name, proving nothing lives between them. The catch: this lets anyone "walk" the zone by chaining NSEC records to enumerate every name. NSEC3 fixes that by bracketing salted, iterated hashes of names instead of the names themselves, so enumeration costs a brute-force per name.

Algorithms. Early DNSSEC used RSA/SHA-256 (algorithm 8) with large keys and signatures. Modern zones prefer ECDSA P-256/SHA-256 (algorithm 13) for ~4× smaller signatures, and Ed25519 (algorithm 15) for fast, compact EdDSA. Smaller signatures directly shrink the amplification and packet-fragmentation problems.

Key rollover schemes. Rotating a ZSK uses pre-publish (publish the new key, wait a TTL, switch signatures, retire the old key). Rotating a KSK uses double-signature and must be choreographed with the parent's DS update — the only step DNSSEC can't do unilaterally. The 2018 root KSK rollover (KSK-2010 → KSK-2017) was the first ever and was deliberately delayed a year over fears that non-updating resolvers would break.

DANE. Once you trust DNS answers, you can put a TLS certificate fingerprint in a TLSA record and authenticate web/email servers without the CA system. DANE is the headline payoff of DNSSEC and a major reason mail providers sign their zones.

Common bugs and edge cases

  • Expired RRSIGs. The single most common DNSSEC outage. Signatures carry an inception and expiration timestamp; if your re-signing cron fails and they lapse, every validating resolver returns SERVFAIL and the domain vanishes for ~a third of the internet — even though the records are correct.
  • Broken parent handoff. You roll a KSK but forget to push the new DS to the registrar, or push it too early. Now the parent's DS hashes a key that no longer signs the zone, and the chain snaps. This is the most error-prone manual step in the whole system.
  • Clock skew. Validation is time-sensitive: a server with a wrong clock can reject signatures that are perfectly valid (or accept expired ones). Both validators and signers need NTP.
  • Treating Bogus as Insecure. An unsigned zone is legitimate; a zone with a bad signature is an attack or a misconfiguration. A resolver that downgrades Bogus to Insecure (and serves the answer) silently defeats the entire point of DNSSEC.
  • Packet fragmentation. Large signed UDP responses get fragmented at the IP layer, fragments get dropped by middleboxes, and the query mysteriously fails. EDNS0 buffer sizing and TCP fallback exist to handle this, but misconfigured firewalls that block DNS-over-TCP cause intermittent failures that are brutal to debug.
  • Algorithm downgrade and "ZSK = KSK" shortcuts. Using a single CSK (Combined Signing Key) is fine if automated, but hand-managed zones that reuse one tiny key and never rotate it weaken the whole chain. Match the algorithm in the DS to the algorithm of the published DNSKEY, or validation silently treats the zone as insecure.

Frequently asked questions

Does DNSSEC encrypt my DNS queries?

No. DNSSEC provides authentication and integrity, not confidentiality. Anyone on the path can still read which domains you look up; DNSSEC only proves the answer is genuine and unmodified. To hide the query itself you need DNS-over-HTTPS or DNS-over-TLS, which are orthogonal to DNSSEC.

What is the difference between a DNSKEY, a DS record, and an RRSIG?

DNSKEY is the zone's public key, published inside the zone. RRSIG is a signature over a record set, made with the matching private key. DS (Delegation Signer) is a hash of the child's DNSKEY, published in the parent zone — it's the link that lets the parent vouch for the child, forming the chain of trust.

Why does DNSSEC use two keys — a KSK and a ZSK?

The Key Signing Key (KSK) only signs the DNSKEY record set and is what the parent's DS record points to; the Zone Signing Key (ZSK) signs all the actual data. Splitting them lets you rotate the smaller, frequently-used ZSK without ever touching the parent — a parent DS update is slow and manual, so you want to avoid it.

How does DNSSEC prove a name does NOT exist?

You can't sign a record that isn't there, so DNSSEC signs the gaps. NSEC returns the two existing names that alphabetically bracket the missing one, proving nothing lives between them. NSEC3 does the same over salted hashes of names to stop attackers from walking the whole zone.

If DNSSEC is so good, why is adoption still low?

Roughly 90% of the root and TLDs are signed, but only about a third of resolvers validate and only a few percent of second-level domains sign. The friction is operational: expired RRSIGs cause hard outages, the parent DS handoff is manual, and HTTPS/TLS already authenticate the web server, so many operators see weak marginal benefit.

What happens if a zone's signatures expire?

The zone goes dark for validating resolvers — they return SERVFAIL and the domain becomes unreachable for a large slice of the internet, even though the records are technically still correct. Expiry is the single most common DNSSEC outage; it has taken down government TLDs. Automated re-signing well before the RRSIG expiration is mandatory.