How to check if dnssec is enabled

A zone is signed when its parent publishes a DS record and the zone itself publishes matching DNSKEY records. Windows nslookup cannot ask for either type. Query a DNS over HTTPS resolver with curl instead, type=DS, and read the Answer array: one DS for example.com, key tag 2371, algorithm 13.

Why check this

Run this before a DNS cutover, after a registrar transfer, and after a signing key change. The failure it prevents is a domain that resolves on your machine and fails for part of the internet: the parent still publishes a DS for a key the new provider does not hold, so validating resolvers return SERVFAIL while the rest keep answering. A tester on a resolver that does not validate sees a working site.

Reading a domain you do not own is safe. Every command asks a resolver about a name, and no packet reaches the domain in the query.

Prerequisites

Steps

  1. Step 1.

    Ask nslookup for the DS record of a signed zone.

    nslookup -type=DS example.com 8.8.8.8
    
    Non-authoritative answer:
    Server:  dns.google
    Address:  8.8.8.8
    
    Name:    example.com
    Addresses:  2606:4700:10::6814:179a
      2606:4700:10::ac42:93f3
      104.20.23.154
      172.66.147.243
    
    unknown query type: DS

    Read the last line first. This nslookup has no DS type, so it sent a type A query and printed four addresses that say nothing about signing. It exits 0.

  2. Step 2.

    Ask a DoH resolver for the same record and keep the response.

    curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=example.com&type=DS' | tee ds.json
    
    {"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,"Question":[{"name":"example.com.","type":43}],"Answer":[{"name":"example.com.","type":43,"TTL":21013,"data":"2371 13 2 C988EC423E3880EB8DD8A46FE06CA230EE23F35B578D64E78B29C3E1C83D245A"}]}

    Status 0 is NOERROR. Answer holds one record of type 43, which is DS. Its fields are the key tag 2371, algorithm 13, digest type 2 for SHA-256, and the digest. The parent zone com trusts a key in example.com.

  3. Step 3.

    Read the keys the zone publishes. Save this as show.js and run curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=example.com&type=DNSKEY' | tee dnskey.json | node show.js.

    let s = '';
    process.stdin.on('data', (d) => (s += d)).on('end', () => {
      const j = JSON.parse(s);
      console.log('Status ' + j.Status + '  AD ' + j.AD + '  CD ' + j.CD);
      const line = (sec, r) =>
        sec.padEnd(10) + r.name + '  type ' + r.type + '  ttl ' + r.TTL + '  ' +
        (r.data.length > 48 ? r.data.slice(0, 48) + '...' : r.data);
      for (const r of j.Answer || []) console.log(line('ANSWER', r));
      for (const r of j.Authority || []) console.log(line('AUTHORITY', r));
    });
    
    Status 0  AD true  CD false
    ANSWER    example.com.  type 48  ttl 2663  256 3 13 MjyZielP0GqniI1+j+wAG/3t0ImDDIlj1CxR0oo...
    ANSWER    example.com.  type 48  ttl 2663  256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8X...
    ANSWER    example.com.  type 48  ttl 2663  256 3 13 kxipjoIbNZDsWqEKaYaGq6fM/XThrRp1ue6AV9R...
    ANSWER    example.com.  type 48  ttl 2663  257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnop...

    Four keys, all algorithm 13. The first number is the flags field: 256 is a zone signing key, 257 a key signing key. Only the 257 key can be the one the DS names. The script cuts each key to 48 characters.

  4. Step 4.

    Prove that the DS in the parent belongs to one of those keys. Save this as ds-match.js and run node ds-match.js example.com dnskey.json ds.json.

    const { createHash } = require('node:crypto');
    const fs = require('node:fs');
    
    function wireName(name) {
      const parts = name.replace(/\.$/, '').split('.');
      const out = [];
      for (const p of parts) { out.push(Buffer.from([p.length]), Buffer.from(p, 'ascii')); }
      out.push(Buffer.from([0]));
      return Buffer.concat(out);
    }
    function keyTag(rdata) {
      let ac = 0;
      for (let i = 0; i < rdata.length; i++) ac += (i & 1) ? rdata[i] : rdata[i] << 8;
      ac += (ac >> 16) & 0xffff;
      return ac & 0xffff;
    }
    const name = process.argv[2];
    const keys = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')).Answer || [];
    const ds = JSON.parse(fs.readFileSync(process.argv[4], 'utf8')).Answer || [];
    for (const k of keys) {
      const [flags, proto, alg, ...b64] = k.data.split(' ');
      const rdata = Buffer.concat([
        Buffer.from([flags >> 8, flags & 0xff, Number(proto), Number(alg)]),
        Buffer.from(b64.join(''), 'base64'),
      ]);
      const digest = createHash('sha256').update(Buffer.concat([wireName(name), rdata])).digest('hex');
      const tag = keyTag(rdata);
      const hit = ds.find((d) => d.data.toLowerCase() === `${tag} ${alg} 2 ${digest}`);
      console.log(`flags ${flags} alg ${alg} tag ${tag} ${hit ? 'matches the DS in the parent' : 'no DS for this key'}`);
    }
    
    flags 256 alg 13 tag 36315 no DS for this key
    flags 256 alg 13 tag 34505 no DS for this key
    flags 256 alg 13 tag 9776 no DS for this key
    flags 257 alg 13 tag 2371 matches the DS in the parent

    The digest is recomputed locally, from the owner name in wire form followed by the key record, and it equals the one the parent publishes. The three zone signing keys have no DS and need none. This is the link a validator checks: a DS existing is not the same as the DS pointing at a key this zone still serves.

  5. Step 5.

    Ask for a DS record that is not there.

    curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=github.com&type=DS'
    
    {"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"github.com.","type":43}],"Authority":[{"name":"com.","type":6,"TTL":784,"data":"a.gtld-servers.net. nstld.verisign-grs.com. 1789161743 1800 900 604800 900"}]}

    There is no Answer key in this response, and Status is still 0. Authority holds the SOA of com: the parent zone answered and has no DS for the name. The delegation is unsigned.

  6. Step 6.

    Ask for an address in the signed zone, with validation turned off by the client.

    curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=example.com&type=A&cd=1'
    
    {"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":true,"Question":[{"name":"example.com.","type":1}],"Answer":[{"name":"example.com.","type":1,"TTL":299,"data":"172.66.147.243"},{"name":"example.com.","type":1,"TTL":299,"data":"104.20.23.154"}]}

    CD is true because the query asked for checking to be disabled, and AD is now false. This is the zone that returned AD true in step 2, minutes earlier. The flag describes the resolver, not the domain.

  7. Step 7.

    Ask a second resolver for the record from step 2.

    curl -sS -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=example.com&type=DS'
    
    {"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,"Question":[{"name":"example.com","type":43}],"Answer":[{"name":"example.com","type":43,"TTL":86400,"data":"2371 13 2 c988ec423e3880eb8dd8a46fe06ca230ee23f35b578d64e78b29c3e1c83d245a"}]}

    Same key tag, algorithm, digest type and digest. The text around them differs: lower case here and upper case at the other resolver, no trailing dot on the owner name, a TTL of 86400 against 21013.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Answer holds a record of type 43 | The parent publishes a DS, so the delegation is signed | Run step 3 and step 4 and confirm a key in the zone still matches that digest | | Status 0, no Answer, an SOA in Authority | The parent has no DS for the name, so the delegation is unsigned | Nothing to fix unless signing was expected. Compare the delegation with How to check ns record | | A DS in the parent and no matching key in step 4 | The parent trusts a key the zone no longer serves | Stop the cutover. Restore the key or remove the DS at the registrar, then wait out the DS TTL | | AD true | This resolver validated this answer | Read it as a resolver verdict, not as a zone property. Step 6 flips it without touching the zone | | AD false on a zone with a DS | This resolver did not validate, or was asked not to | Repeat without cd=1, then at a second resolver, before calling it a fault |

What a validation failure would look like

This page has no capture of one: every domain it may query is either correctly signed or unsigned. In the fields above, a resolver that cannot build the chain answers SERVFAIL with no Answer array, while the same name with cd=1 returns the records. That pair is a signing fault, not an outage.

Common mistakes

Sign: nslookup prints an answer to a DS query and the domain goes into the report as signed.Cause: This nslookup has no DS or DNSKEY type. It sends a type A query instead, prints four addresses, appends one line reading 'unknown query type: DS' after them, and exits 0. A wrapper script that tests the exit status sees a success.
Sign: The AD flag is recorded as the answer to 'is DNSSEC enabled on this domain'.Cause: AD is the resolver's statement about the answer it just sent. Step 6 asks for the same signed zone with cd=1 and gets AD false, and nothing about the zone changed. A resolver that does not validate returns AD false on every answer it gives, signed zone or not, so the flag can be cleared for reasons that have nothing to do with the DS record.
Sign: Two resolvers are compared with a string equality test and the domain is reported as inconsistent.Cause: Step 2 and step 7 hold the same DS. dns.google returns the digest in upper case with a trailing dot on the name, cloudflare-dns.com returns lower case with no trailing dot, and the TTL counts down between reads. Compare the key tag, algorithm, digest type and case-folded digest, never the raw strings.
Sign: A DS query that returns Status 0 and no records is treated as a lookup error and retried.Cause: Status 0 is NOERROR and the retry returns the same thing. Step 5 shows the shape that means no such record: no Answer key, and the SOA of the parent zone in Authority. An unsigned domain is a finding, not a failed query.

Thresholds

86400 s on the DS record for example.com at one resolver, 21013 s at another in the same minute Source: measured in steps 2 and 7 on 2026-09-12, minutes apart. The gap is cache age at the resolver, not a difference in the record, and it sets how long a stale DS survives after a registrar change

What to check next

FAQ

How to check dnssec for a domain?

Query the registrable domain for type=DS, as in step 2. A record of type 43 in Answer means the delegation is signed. A subdomain carries no DS of its own.

How to check dnssec validation?

Send the query twice, plain and with cd=1. The resolver validated when the plain answer has AD true, as in step 2, and was told not to when CD is true, as in step 6.

Can I check if domains are dnssec protected programmatically?

The DoH endpoints answer a plain GET with JSON, so a script reads Answer and looks for type 43. Do not shell out to nslookup: step 1 shows it exiting 0 on a type it does not support.

Verified

Verified by Maks Vernycurl 8.21.0node 22.23.2nslookup Windows 11 build 22631

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate8 minpublished updated Maks Verny