Long time no posts, huh? Time to show some activity again. Recently I decided to write about how cool DNSSEC is at ensuring your “A youtube.com” query response doesn’t get tampered mid-way. However, further research didn’t live up to my expectations and proved most of the topic to be pretty boring (alongside with the Key Signing Ceremony) with primitive cryptography under the hood. There are plenty of articles on how this thing works, e.g. from Cloudflare and Cartoon Network (?). However, a small detail caught my attention - not how the protocol handles existing domain, but rather non-existing ones.
You see, even for an (non-critical) error response, the answer has to be signed to prevent an intermediate party (i.e. attacker) from forging fake NXDOMAIN / NOERROR packets towards the target. You can’t return a single signed “not found” record for all queries either, as this makes the protocol vulnerable to replay attacks. Apart from commonly mentioned DNSKEY, RRSIG and DS records, the protocol includes NSEC / NSEC3 records (RFC 4033, 4034, 4035), which address this issue.
For example, when querying for abcdef.audio (a non-existing domain), we get the following records (+SOA and RRSIG for each record):
dig +dnssec +trace abcdef.audio
aaroncedwards.audio. NSEC abcnews.audio. NS RRSIG NSEC
audio. NSEC 033.audio. NS SOA RRSIG NSEC DNSKEY
Response type: NXDOMAIN
As you might have spotted, the first record contains alphabetically closest (ASCII-based) existing domains in the zone alongside with the record types for aaroncedwards.audio. (record owner). When returned as a query response, the resolver checks all signatures (RRSIG) following the standard DNSSEC procedure. It then makes sure that the queried domain is alphabetically between returned boundaries - only then such an answer is considered a DNSSEC-valid NXDOMAIN. The second record (audio. NSEC 033.audio.) proves there is no wildcard in the given zone, as * character would fall between zone apex aaroncedwards.audio. and abcnews.audio., where, as provided by the authoritative server, no names exist. Final chain:
audio. < 033.audio. < ... < aaroncedwards.audio. < abcdef.audio. < abcnews.audio. < ...
However, you don’t want to leak your zone information like that. A simple walk along the NSEC chain will reveal all existing domains in a zone. NSEC3 (RFC 5155) addresses this problem by storing and presenting name hashes instead of plaintext. The this implementation is already used in 96% of all DNSSEC-enabled TLDs (as of 2026-03-21). It is a bit more sophisticated, but stay with me. Assume the following case:
dig +dnssec +trace abc122112.com
CK0POJMG874LJREF7EFN8430QVIT8BSM.com. NSEC3 1 1 0 - CK0Q35HRS4H76G6CHNB9414CJN6S5UPL NS SOA RRSIG DNSKEY NSEC3PARAM
M0C4BC1D10S3IHV0F9LF1P5JIDC4NR6L.com. NSEC3 1 1 0 - M0C4HVVBEDI7QQSRG6ENIMVTUEPURN9G NS DS RRSIG
3RL2Q58205687C8I9KC9MV46DGHCNS45.com. NSEC3 1 1 0 - 3RL2VRNHRTNQP2ACB7JK3K0OGV4ST3U3 NS DS RRSIG
Response type: NXDOMAIN
Now, that’s some gibberish, but let’s prove by hand that abc122112.com does not exist:
-
The first record is the hash for the longest existing suffix in the QNAME (closest encloser). We know that
.comexists, but notabc122112.com, which means the hash for.comshould beCK0POJMG874LJREF7EFN8430QVIT8BSM, exactly as in the first record:HASH = BASE32HEX(SHA1(WIRE_FORMAT_DOMAIN_NAME)) = BASE32HEX(SHA1(\x03 c o m \x00)) = CK0POJMG874LJREF7EFN8430QVIT8BSM
I did not pull SHA1 out of my… pocket; the algorithm specified near the record type:
NSEC3 1 1 0, where first1is the algorithm, second1is the Opt-Out flag (see below),0is the number of additional times the hashing function is reapplied and-means “no salt”. -
The next two entries are similar to ones returned in the legacy NSEC records, but with hashes instead of plaintext domain names. The next closer name proof assures that the immediate child of the closest encloser (which shares the suffix of the QNAME) does not exist, meaning the hash of
abc122112.comis somewhere betweenM0C4BC1D10S3IHV0F9LF1P5JIDC4NR6LandM0C4HVVBEDI7QQSRG6ENIMVTUEPURN9Gor3RL2Q58205687C8I9KC9MV46DGHCNS45and3RL2VRNHRTNQP2ACB7JK3K0OGV4ST3U3. Let’s check:HASH = BASE32HEX(SHA1(\x09 a b c 1 2 2 1 1 2 \x03 c o m \x00)) = M0C4BNL8H3LL50L7F1AL31CH22NUA98L
So,
M0C4BC1D10S3IHV0F9LF1P5JIDC4NR6L < M0C4BNL8H3LL50L7F1AL31CH22NUA98L < M0C4HVVBEDI7QQSRG6ENIMVTUEPURN9G, as expected. -
The remaining entry proves there is no wildcard for the longest existing suffix (i.e.
*.com.), as its hash would fall in this range:HASH = BASE32HEX(SHA1(\x01 * \x03 c o m \x00)) = 3RL2QNA1V2SN1B35AIU0JPECRGH26852
which is right between
3RL2Q58205687C8I9KC9MV46DGHCNS45and3RL2VRNHRTNQP2ACB7JK3K0OGV4ST3U3.
Note that the hashes are sorted alphabetically in this case, and not the underlying plaintext domain names.
Overall, you can think about both implementations as being linked lists, where each entry (record) is an interval and points to the next one. The last entry (e.g z.com) points to the first entry (.com), providing a circular path. Naturally, when creating a new record with a unique name in a zone (e.g. b.com), a DNSSEC-enabled authoritative server inserts an element into an existing interval by updating its preceding record (changing a.com. NSEC c.com. to a.com. NSEC b.com.) and creating a new one, pointing to the next entry (b.com. NSEC c.com.). Of course, every change is signed with the ZSK. To keep it short, I’ll summarize the pros and cons of each approach:
NSEC advantages:
- Pre-populated, pre-signed NSEC records. Least cryptographic runtime overhead.
Weaknesses:
- Additional DB / zone lookup required by the auth. server to find domain’s closest neighbors.
- A single NSEC entry is required for each domain in a zone, which is particularly bad for very large zones (e.g. TLDs).
- Performing an iterative NSEC lookup for all linked domains essentially reveals all domains in a given zone (+ the record types they contain).
NSEC3 advantages:
- Reveals less information about domains in a zone, just enough for a resolver to verify authenticity of an answer.
- Pre-populated, pre-signed NSEC3 records.
- Opt-Out flag allows reducing the number of NSEC3 records only to authoritative data + secure delegations (with a
DSrecord in the current zone). This is particularly useful, as most second-level domains do not bother to use DNSSEC, so the TLD doesn’t need to create respective records for them.
Weaknesses:
- Still a lot of NSEC3 entries, more DB / zone lookups.
- Larger response (2 or 3 NSEC3 + SOA + 4 RRSIGs).
- More cryptographic processing overhead for all involved parties (DNSSEC-enabled client, resolver and auth. server).
- Still not 100% secure against domain enumeration. A walk in the hash space is still possible; a subsequent offline brute-force / wordlist attack on collected hashes is still possible.
But is it possible to defeat some of the common weaknesses? This is where “White Lies” steps in [I really like this name]. The approach relies on dynamically generated NSEC (RFC 4470) / NSEC3 (RFC 7129 Appendix B) records instead of using pre-computed ones, which eliminates additional zone lookups and reduces its size. Because the auth. server knows the domain doesn’t exist, it can generate very narrow NSEC(3) boundaries for this query (e.g. for NSEC3: prev.: HASH(QNAME) - 1, next: HASH(QNAME) + 1) and sign them on-the-fly.
“White Lies” advantages:
- Zone file smaller, as no additional records are needed.
- Response does not reveal any information about existing domains; names with returned hashes may / do not exist in the zone.
Weaknesses:
- More cryptographic processing overhead for all involved parties (especially the auth. server).
- Private signing key (e.g. ZSK) has to be kept online (on the auth. server) to sign dynamically generated records.
- Response size is still large, as it has to contain all 2-3 NSEC(3) records + RRSIGs to satisfy the resolver’s validation chain.
But this is not the end yet. The proposed approach was still quite compute-heavy, especially for growing / large zone hosters. Apart from that, large responses could cause UDP transmission errors and were perfect candidates for amplification attacks (at least back then). In 2016 Cloudflare decided to sort out this cryptographic mess themselves. Why waste bandwidth and compute on extra records if one would be enough? The result was the “Black Lies” [another great one] approach, later renamed to “Compact Denial of Existence in DNSSEC” (RFC 9824). By combining the online signing approach with a single NSEC record they were able to greatly reduce the processing overhead:
dig +dnssec +trace bogus.ietf.org
bogus.ietf.org. NSEC \000.bogus.ietf.org. RRSIG NSEC TYPE128
Response type: NOERROR
This entry is just enough to prove non-existance of bogus.ietf.org without revealing further zone details.
But did you notice something? The response is a NOERROR, whereas it should have been a NXDOMAIN. To prevent existing resolver implementations from breaking / failing during DNSSEC validation, the implementation returns a NOERROR response with the TYPE128 (NXNAME) bit set in the NSEC type bitmap, which indicates that this response should be treated as NXDOMAIN by the resolver. This is due to the fact that a real NXDOMAIN response requires at least 2 NSEC records (closest encloser / next closer name and denial of wildcard), whereas the current solution only requires one. For a truly non-existing record type for the given domain, all other record types (except for the queried one) are returned, and no NXDATA is appended:
dig +tcp +dnssec +trace CAA ietf.org
ietf.org. NSEC \000.ietf.org. A NS SOA PTR HINFO MX TXT AAAA ...
Response type: NOERROR
This does not mean they really exist, but only that the queried one doesn’t. And again - no information leak.
This was a brief overview of the evolution of denials in DNSSEC and the “lies” that appeared in the process. Not gonna lie [hehe], this was a tough one. Initially, this text was intended to be published as a short post on a Telegram channel, but its length has substantially increased since. I didn’t expect the rabbit hole to go that deep - now I’m unsure I covered everything of such a small small area in the DNS world. Should you remember all those nuances in such detail? I don’t think so. But, if you are a cybersecurity guy - NSEC(3) enumeration is definitely a viable attack vector if you are dealing with DNS domain space (only if AXFR didn’t help). Let me know what you think about the summary and the topic in general. Stay tuned!
Glossary
- QNAME (Query Name): The exact domain name that your computer is asking the DNS system to look up, such as
abc122112.com. - NXDOMAIN / NOERROR: Status codes returned by a DNS server indicating whether a lookup succeeded.
NXDOMAINmeans the domain does not exist anywhere in the registry, whileNOERRORmeans the server processed the request without errors (even if the specific record type requested turns out to be empty). - NSEC / NSEC3: Security records used by DNSSEC to prove that a requested domain or record type does not exist. While
NSECproves non-existence by pointing to the next alphabetically sorted real name,NSEC3protects privacy by converting those names into scrambled cryptographic hashes. - Domain (name): A human-readable text label (such as
ietf.org) used to navigate the internet in place of complex numerical IP addresses. Domain names are organized hierarchically, read from right to left, with each level separated by a dot. - Root zone: The very top level of the internet’s domain hierarchy, conceptually represented by an invisible dot (
.) at the end of every web address. It acts as the master directory directing resolvers to top-level domain registries like.com,.org, or.net. - Zone (in general): A distinct administrative bucket or slice of the internet’s naming system managed by a single organization or administrator. For example, the
.comregistry manages one zone, while a private business manages the smaller sub-zone forexample.com.