DNS cache poisoning is an attack that gets a caching resolver to accept and store a forged answer, so every client that asks afterwards is sent to the attacker's address. It worked because a classic DNS reply was matched to its query mainly by a 16 bit transaction ID, which an off path attacker could guess.
Since 2008 defenders have added secrets the attacker must also guess (a random source port, mixed case query names, cookies), closed side channels that leaked those secrets (SAD DNS in 2020), fixed logic bugs in what a resolver agrees to cache (MaginotDNS in 2023), and, where zones are signed and resolvers validate, made forgery detectable with DNSSEC. This history goes one level deeper than our DNS protocol and resolver hardening explainer. It walks through each generation of the attack in concept, the fix that answered it, what resolvers changed by default, how encrypted DNS changes the picture, and the exact commands to check Unbound, BIND, Windows DNS Server and a Linux client today. Every date, default and figure is taken from the RFC, advisory, paper or vendor document listed under Sources.
Key takeaways
- A forged reply is accepted if it arrives before the real one and matches the query's addresses, ports, question and 16 bit ID. CERT/CC's 2008 note puts the ID alone at an average of 32,768 guesses when it is truly random.
- The 2008 Kaminsky disclosure (CERT/CC VU#800113, CVE-2008-1447, public 8 July 2008) removed the old speed limit. Instead of waiting for a cached record's TTL to expire, an attacker could trigger a fresh lookup for a new, nonexistent name every time and carry the real target inside the forged referral.
- Per query source port randomisation, required by RFC 5452 (January 2009), multiplies the guessing space by up to about 64,000. RFC 5452's own numbers move a 50% success time from about 7 seconds to about 116 hours at the same packet rate.
- Later attacks did not break that maths. They went around it: IP fragmentation let attackers replace the second fragment of a reply without guessing anything (Herzberg and Shulman, 2012), SAD DNS used an operating system ICMP rate limit to discover the source port (ACM CCS 2020), and MaginotDNS abused inconsistent bailiwick checks in servers that both forward and recurse (USENIX Security 2023).
- DNSSEC is the only fix that makes a forged answer detectable rather than merely hard to land. It only helps for signed zones, queried through a resolver that validates and refuses bogus data.
- Resolver defaults moved: BIND and Unbound advertise a 1,232 byte EDNS buffer (DNS Flag Day 2020), BIND validates with a built in root trust anchor by default, and DNS Cookies (RFC 7873, RFC 9018) add a 64 bit secret when both sides support them.
- Encrypted DNS (DoT, DoH, DoQ) protects the hop from your device to your resolver. It does nothing for the resolver to authoritative server hop that SAD DNS and most poisoning attacks target.
- The root zone's new key signing key, KSK-2024 (key tag 38696), began signing the root on 11 October 2026. A validating resolver without it in its trust anchors will fail closed, so check it now.
What is DNS cache poisoning, and why did a 16 bit ID make it possible?
A recursive resolver does the legwork of DNS for its clients. When your laptop asks for the address of a name, the resolver asks the root servers, then the servers for the top level domain, then the zone's authoritative servers, and caches each answer for its time to live (TTL). Every later client that asks for the same name gets the cached answer without any new network traffic. That is the whole point of a cache, and also the whole risk: if one forged record gets in, it is served to everyone for as long as its TTL says.
The classic DNS message format in RFC 1035 (November 1987) gives every query a 16 bit identifier, and the reply copies it back so the requester can match replies to outstanding queries. Over UDP there is no handshake. The resolver sends one packet and waits for one packet back. Anyone on the internet who can send a UDP packet with a spoofed source address, claiming to come from the authoritative server, can try to answer first.
RFC 5452 lists what a forged reply has to match to be accepted:
- The question section must match the outstanding query (name, type and class).
- The reply must arrive at the right address and port: the resolver's address and the source port the resolver used for the query.
- The reply must appear to come from the address the query was sent to.
- The transaction ID must match.
- It must arrive before the real answer.
An off path attacker cannot see the query, so it has to guess the unknowns. In the 1990s and 2000s most resolvers used one fixed source port, often 53 itself or a port chosen once at start up, so RFC 5452 notes that the port "often requires no guess work". The resolver's address and the authoritative server's addresses are public. That left the 16 bit ID, 65,536 values, and CERT/CC's 2008 note gives the average number of guesses for a properly random ID as 32,768. Worse, CERT/CC also cites 2007 research by Amit Klein showing that several implementations generated IDs that were not random enough, and older implementations that sent several identical queries for the same record at once, which enabled a "birthday attack" where any forged reply could match any of the outstanding queries.
The speed limit that used to exist
Before 2008, the attacker faced one big practical limit. If the target record was already in the cache, the resolver would not ask for it again until the TTL ran out. A single failed race meant waiting, perhaps a day, for the next window. RFC 5452 states it directly: the TTL of the target "determines how often a window of opportunity is available".
Worked example 1: the RFC 5452 arithmetic for a fixed port
RFC 5452 gives a formula for the chance that a single window of opportunity is won:
P_s = (D * R * W) / (N * P * I)
D identical outstanding queries (typically 1)
R forged packets per second the attacker sends
W window length in seconds (often 0.1 s, the authoritative server's response time)
N authoritative servers for the domain (averages around 2.5)
P source ports in use (often 1 before 2008)
I distinct transaction IDs (65,536)
With the RFC's common values, N * P * I / (D * W) = 2.5 * 1 * 65,536 / 0.1 = 1,638,400, so P_s = R / 1,638,400.
Take the RFC's own attacker sending 7,000 forged packets per second, which it says is about 4.5 Mbit/s:
- Chance per window: 7,000 / 1,638,400 = 0.00427, a little under half a percent.
- With a 3,600 second TTL there are 24 windows a day. Chance of at least one success in a day:
1 - (1 - 0.00427)^24 = 0.098, about 10%. - Over a week, 168 windows:
1 - (1 - 0.00427)^168 = 0.51, about 50%.
Those are exactly the figures RFC 5452 reports: a 10% chance in the first 24 hours and 50% after a week. The RFC adds that with a 60 second TTL the 10% level falls after 24 minutes and 50% after less than 3 hours. Short TTLs, which many operators use for agility, made poisoning much more practical.
What did the 2008 Kaminsky disclosure change?
On 8 July 2008, CERT/CC published Vulnerability Note VU#800113, "Multiple DNS implementations vulnerable to cache poisoning", and coordinated patches from many vendors on the same day. NVD's entry for CVE-2008-1447, also dated 8 July 2008, names the problem "the Kaminsky bug" after researcher Dan Kaminsky, and describes it as a birthday attack "that uses in-bailiwick referrals" against recursive resolvers, made possible by insufficient randomness of transaction IDs and source ports. Microsoft's bulletin MS08-037 for the Windows DNS client and server was published the same day.
How it worked, in concept
The Kaminsky technique removed the TTL speed limit. Instead of racing for the target name itself, which might already be cached, the attacker made the resolver look up a different name in the same zone that had never been asked for, such as a random label under the target domain. That name is never in the cache, so the resolver always sends a fresh query, and the attacker can trigger a new race immediately after losing one.
The forged reply did not just answer the random name. It carried a referral or authority data, records saying which servers are authoritative for the parent zone and what their addresses are, pointing at servers the attacker controls. Because those records were "in bailiwick", meaning they belonged to the zone the resolver was asking about, resolvers of the time accepted and cached them. Once one race was won, the attacker did not own one name; it owned the resolver's view of the whole domain.
RFC 5452 section 8.1 notes that these "repeated and changing queries" reduce the effective TTL to zero. That is the change that turned a slow, noisy attack into a fast one.
Why source port randomisation helped
The CERT/CC note named the practical mitigation within the existing protocol: per query source port randomisation, which it said adds approximately 16 bits of randomness the attacker must guess. The idea predates 2008. RFC 5452's acknowledgements say source port randomisation in DNS was "first implemented and possibly invented by Dan J. Bernstein". What changed in July 2008 is that every major implementation shipped it at once.
RFC 5452, published in January 2009 as a Standards Track update to RFC 2181, turned it into a requirement. Resolver implementations MUST use an unpredictable source port for outgoing queries "from the range of available ports (53, or 1024 and above) that is as large as possible and practicable", MUST use multiple ports simultaneously, and MUST match replies against the source and destination address, the ports, the ID and the question. Resolvers with several IP addresses SHOULD also pick among them unpredictably.
Worked example 2: what the port adds
Use RFC 5452 section 8.1, which covers the Kaminsky style case where the effective TTL is zero. The RFC's figures for this case work out when you take a single authoritative server, so the per second success rate is R / (P * I).
With one fixed source port:
- Success rate per second: 7,000 / 65,536 = 0.107.
- Time to a 50% chance:
ln 2 / 0.107 = 6.5 seconds. RFC 5452 rounds this to "within 7 seconds".
With 64,000 randomised ports:
- Success rate per second: 7,000 / (64,000 * 65,536) = 0.00000167.
- Time to a 50% chance:
ln 2 / 0.00000167 = 415,000 seconds, about 115 hours. RFC 5452 says "around 116 hours".
The RFC also notes that matching the earlier one week, 50% result against a randomised port would need more than 285 Gbit/s of forged traffic. That is the size of the win, and it is why every later attack in this history is about getting around the port, not out guessing it.
What came after 2008: the later variants in concept
Mixed case query names: the 0x20 defence (2008)
In March 2008, months before the Kaminsky disclosure, Paul Vixie and David Dagon published an Internet Draft, "Use of Bit 0x20 in DNS Labels to Improve Transaction Identity". DNS name matching is case insensitive, but almost every authoritative server copies the question back exactly as it received it. In ASCII, upper and lower case letters differ by one bit, the 0x20 bit. A resolver can randomly flip the case of each letter in the name it sends, for example wWw.ExaMPle.cOm, and accept only a reply whose question section has exactly the same casing.
The draft's key observation is that this adds one bit of identity per letter, so longer names are better protected. It never became an RFC (the datatracker lists it as an expired individual draft), but resolvers implemented it. Unbound's use-caps-for-id option describes it as "an experimental implementation of draft dns-0x20", default off, with a caps-exempt list for zones whose servers do not preserve case.
Worked example 3: how many bits does 0x20 add?
Only letters can change case, so digits, hyphens and dots add nothing.
www.example.comhas 3 + 7 + 3 = 13 letters, so 13 extra bits, a factor of 2^13 = 8,192.- A short name such as
ab.iohas 4 letters, so only 4 extra bits, a factor of 16.
Combined with a randomised port and ID, a 13 letter name raises the search space from about 65,536 x 64,000 (about 4.2 billion) to about 34 trillion. The draft itself noted the cost: authoritative servers that do not preserve case will see repeated queries, and resolvers have to fall back, which is why it remains an option rather than a default.
Fragmentation based poisoning (2012 onwards)
In May 2012, Amir Herzberg and Haya Shulman posted "Fragmentation Considered Poisonous" to arXiv. Their abstract describes practical poisoning by off path, spoofing attackers who "exploit large DNS responses that cause IP fragmentation", responses that were becoming common because DNSSEC signatures make answers bigger.
The concept is simple once you see it. When a UDP reply is too big for the path, IP splits it into fragments. The transaction ID, the ports and the question are all in the first fragment. The second fragment carries only later parts of the answer and no DNS level secrets. An attacker who can predict the IP fragment identifier and send a forged second fragment ahead of the real one can have the resolver's operating system reassemble a reply that is half real, half forged. The real first fragment passes every check, so port randomisation, IP randomisation and 0x20 do not help; the paper says its attacks "circumvent resolver-side defenses" of exactly that kind.
The fix had to come from the transport layer. DNS Flag Day 2020, a coordinated change by DNS software vendors and operators dated 1 October 2020, set the recommended default EDNS buffer size to 1,232 bytes: an IPv6 minimum MTU of 1,280 minus 48 bytes for IPv6 and UDP headers. Replies that do not fit must set the truncation bit so the client retries over TCP. RFC 9715 (January 2025, Informational) later wrote the guidance down for UDP requestors and responders: avoid IPv6 fragmentation, keep payloads to at most 1,400 bytes or the path MTU if smaller, and have requestors drop fragmented DNS over UDP replies "to avoid cache poisoning attacks". Today both BIND (edns-udp-size and max-udp-size) and Unbound (edns-buffer-size and max-udp-size) default to 1,232.
SAD DNS (2020): a side channel finds the port
At ACM CCS in November 2020, Keyu Man, Zhiyun Qian, Zhongjie Wang, Xiaofeng Zheng, Youjun Huang and Haixin Duan presented "DNS Cache Poisoning Attack Reloaded: Revolutions with Side Channels", which won a distinguished paper award. The project site describes SAD DNS (Side channel AttackeD DNS) as "a revival of the classic DNS cache poisoning attack" that uses network side channels present in Linux, Windows, macOS and FreeBSD.
In concept: when a UDP packet reaches a closed port, the operating system answers with an ICMP "port unreachable" message, and operating systems cap how many such ICMP messages they send per unit of time across the whole host. The site says the root cause is that global rate limit for outgoing ICMP described in RFC 1812. Because the limit is shared, an attacker can send probes to a batch of candidate ports on the resolver and then check, from its own address, whether the global ICMP budget was used up. If one probe hit an open port (the one the resolver is using for an outstanding query), one fewer ICMP was sent, and the attacker learns that its batch contained the port. By narrowing batches it infers the port, then only has to guess the 16 bit ID: the 2008 problem again. The researchers also describe extending the attack window by keeping the real authoritative server from answering quickly.
The site gives the measured scope at publication: 35% of open resolvers vulnerable, 12 of 14 public resolvers checked, and 4 of 6 home routers tested. It lists Linux 3.18 to 5.10, Windows Server 2019 (1809) and newer, macOS 10.15 and newer, and FreeBSD 12.1.0 and newer as affected in their tests, and notes that Windows blocked the outgoing ICMP by default.
The fixes:
- Linux kernel. The researchers worked with the kernel security team on a patch that randomises the global ICMP rate limit so it no longer leaks a clean signal. It is CVE-2020-25705, which NVD published on 17 November 2020 and describes as a flaw that lets an off path attacker "effectively bypass source port UDP randomization". The patch landed in 5.10 and was backported to stable kernels.
- Configuration. Blocking outgoing ICMP port unreachable messages from the resolver host removes the side channel entirely.
- More secrets in the message. The site lists DNSSEC, 0x20 encoding and DNS cookies as ways to add entropy the side channel cannot reveal, plus shorter timeouts for outstanding queries.
- NAT is not a fix. The site warns that a NAT gateway in front of a patched resolver can make it vulnerable again, because the NAT itself can be port scanned.
The same group followed up at CCS 2021 with "DNS Cache Poisoning Attack: Resurrections with Side Channels" (SAD DNS 2.0), using a different ICMP based side channel; the project's link list includes CVE-2021-20322 for Linux. The protocol maths was fine; the leak was in the host network stack.
MaginotDNS (2023): a logic flaw in what gets cached
At USENIX Security 2023, Xiang Li, Chaoyi Lu, Baojun Liu, Qifan Zhang, Zhou Li, Haixin Duan and Qi Li presented "The Maginot Line: Attacking the Boundary of DNS Caching Protection". It targets a specific deployment: a DNS server that acts as both a forwarder (sending some queries to an upstream resolver) and a recursive resolver (resolving other queries itself). The paper calls these CDNS servers.
The cornerstone it attacks is the bailiwick check, the rule that a server only caches records that fall within the zone it asked about. The paper found that the checks applied in forwarding mode and in recursive mode were inconsistent in several implementations, so records accepted on the forwarding path could end up trusted on the recursive path. The authors report that the attack could take over entire zones, including top level domains, that CDNS servers were up to 41.8% of the open DNS servers they probed, and that at least 35.5% of those were vulnerable. The paper's references list three CVEs: CVE-2021-25220, CVE-2021-43105 and CVE-2022-32983.
For BIND, CVE-2021-25220 is ISC's advisory "DNS forwarders: cache poisoning vulnerability". It affects BIND 9.11.0 to 9.11.36, 9.12.0 to 9.16.26 and 9.17.0 to 9.18.0 when forwarding is configured, with a fix in 9.11.37, 9.16.27 and 9.18.1. ISC scored it 6.2 and was not aware of active exploits. The workaround is to remove either all forwarding or all recursion from a server.
The lesson: no amount of entropy fixes a server that caches records it should have rejected, and mixing roles on one box creates two rule sets that must agree.
A dated timeline
| Date | Event | Source |
|---|---|---|
| Nov 1987 | RFC 1035 defines the message format with a 16 bit ID | RFC 1035 |
| Mar 2005 | RFC 4033 to 4035 define DNSSEC as it is deployed today | RFC 4033 |
| 2007 | Amit Klein shows weak transaction ID generation in several implementations | CERT/CC VU#800113 |
| Mar 2008 | 0x20 draft by Vixie and Dagon | draft-vixie-dnsext-dns0x20-00 |
| 8 Jul 2008 | VU#800113, CVE-2008-1447 (Kaminsky) and MS08-037 published; coordinated patches add source port randomisation | CERT/CC, NVD, Microsoft |
| Jan 2009 | RFC 5452 makes port randomisation and strict reply matching a requirement | RFC 5452 |
| 15 Jul 2010 | First root key signing key (KSK-2010, key tag 19036) valid; the root zone is signed | IANA root-anchors.xml |
| 17 May 2012 | "Fragmentation Considered Poisonous" posted | arXiv 1205.4011 |
| May 2016 | RFC 7873 DNS Cookies and RFC 7858 DNS over TLS | RFC 7873, RFC 7858 |
| 2 Feb 2017 | KSK-2017 (key tag 20326) valid from this date; it replaced KSK-2010 in the 2018 rollover | IANA root-anchors.xml, ICANN |
| Oct 2018 | RFC 8484 DNS over HTTPS | RFC 8484 |
| 1 Oct 2020 | DNS Flag Day 2020: 1,232 byte default EDNS buffer | dnsflagday.net |
| Nov 2020 | SAD DNS at ACM CCS 2020; CVE-2020-25705 published 17 Nov | saddns.net, NVD |
| Apr 2021 | RFC 9018 interoperable server cookies | RFC 9018 |
| Nov 2021 | SAD DNS 2.0 at ACM CCS 2021; RFC 9156 QNAME minimisation | saddns.net, RFC 9156 |
| Mar 2022 | CVE-2021-25220 (BIND forwarders) published | NVD, ISC |
| May 2022 | RFC 9250 DNS over QUIC | RFC 9250 |
| 2023 | MaginotDNS at USENIX Security 2023 | USENIX |
| Jan 2025 | RFC 9715 IP fragmentation avoidance in DNS over UDP | RFC 9715 |
| 11 Oct 2026 | KSK-2024 (key tag 38696) begins signing the root zone; ICANN reports completion on 15 Oct | ICANN |
Attack comparison table
| Attack | Year | What the attacker had to guess or bypass | Attacker position | Main fix | What still matters |
|---|---|---|---|---|---|
| Classic ID guessing | Before 2008 | 16 bit ID; port often fixed; one race per TTL | Off path, can spoof source IP | Random IDs, no duplicate outstanding queries | Long TTLs slowed it, short ones helped it |
| Kaminsky | 2008 | 16 bit ID, but unlimited races using random names; in bailiwick referral carries the payload | Off path, can spoof | Per query source port randomisation (RFC 5452); stricter acceptance of referral data | Without DNSSEC, still probabilistic |
| 0x20 (defence) | 2008 | Adds one bit per letter of the query name | n/a | Optional resolver feature | Short names gain little; some servers break it |
| Fragmentation | 2012 | Nothing at DNS level; predict IP fragment ID and replace the second fragment | Off path, can spoof | Small EDNS buffer (1,232), TCP fallback, drop fragmented replies (RFC 9715) | DNSSEC also detects the forged data if validated |
| SAD DNS | 2020 | Infers the source port via a global ICMP rate limit side channel, then guesses the ID | Off path, can spoof | Randomised ICMP limit in Linux (CVE-2020-25705), block outgoing ICMP unreachable, cookies, 0x20, DNSSEC | NAT gateways in front of resolvers |
| MaginotDNS | 2023 | Nothing cryptographic; exploits inconsistent bailiwick checks between forwarder and resolver roles | Off path or via a controlled domain | Patched software (for BIND, 9.11.37, 9.16.27, 9.18.1); separate forwarding and recursion | DNSSEC validation limits impact for signed zones |
How do DNSSEC validation and its deployment gaps fit in?
Every defence above makes a forged answer harder to land. DNSSEC is the only one that makes a forged answer detectable. RFC 4033 (March 2005) describes it as adding data origin authentication and data integrity: zone owners sign their records, the parent zone vouches for the child's key with a DS record, and a validating resolver follows that chain of signatures from a trust anchor it already holds, normally the root key signing key.
If a forged record arrives, it will not carry a valid signature chaining to the root, and a validating resolver marks it bogus and returns SERVFAIL to the client instead of caching it. That holds whether the forgery came from an ID guess, a forged fragment, a side channel or a bailiwick bug, which is why the SAD DNS site lists DNSSEC as a fix and why the 2008 CERT/CC note said mitigations short of protocol changes "such as those that the DNS Security Extensions (DNSSEC) introduce" cannot completely prevent poisoning.
The gaps
DNSSEC only protects what is both signed and validated:
- Unsigned zones get no protection. If the domain you are resolving is not signed, a validating resolver treats the answer as insecure and accepts it. Signing is a choice each zone owner makes, so check the names you depend on rather than assuming.
- Resolvers that do not validate get no protection. Setting the DO bit and receiving signatures is not validation. BIND's documentation points out that even with
dnssec-validation no, the resolver still sets DO on outgoing queries. A resolver must actually check signatures and withhold bogus data. - Permissive or downgradable modes weaken it. Unbound's
val-permissive-modepasses bogus data to clients. systemd-resolved'sDNSSEC=allow-downgrademode, by its own documentation, is "vulnerable to downgrade attacks" because it turns validation off if the upstream server does not appear to support DNSSEC. The SAD DNS FAQ makes the same point: a resolver must "implement strict DNSSEC check" to stop off path attacks. - The last hop is usually unprotected. Most stub resolvers on laptops and phones trust the AD (authenticated data) bit from their recursive resolver. If the path between them is not protected, an on path attacker can alter answers and set the bit. This is where encrypted DNS earns its place.
The 2026 root key rollover
The root zone's trust anchor changes rarely, and each change is a test of whether validating resolvers are kept current. IANA's published trust anchor file lists KSK-2010 (key tag 19036, valid from 15 July 2010), KSK-2017 (key tag 20326) and KSK-2024 (key tag 38696, valid from 18 July 2024). ICANN's rollover page reports that KSK-2024 "began signing the DNS root zone" at 14:00 UTC on 11 October 2026, with no widespread disruption observed, and that the rollover was reported complete on 15 October 2026. ICANN's reminder to resolver operators is specific: verify that key tag 38696 is present, and "do not assume automatic trust-anchor updates succeeded". The commands in the walkthrough below show how.
What did resolvers change after each attack?
Strict reply matching and port randomisation
Every maintained resolver now picks a random source port per query and checks addresses, ports, ID and question. In BIND's current grammar, query-source takes an address but no port argument, so the old habit of pinning queries to port 53 for a firewall rule is no longer expressible there. Unbound opens outgoing-range ports per thread and lets operators exclude ranges with outgoing-port-avoid. Windows DNS Server uses a socket pool; the Get-DnsServerSetting example output in Microsoft's documentation shows SocketPoolSize : 2500.
Cache locking (Windows) and not overwriting good data
Microsoft's Set-DnsServerCache documentation describes cache locking: the percentage of a TTL during which the server will not overwrite a cached entry. The default, 100, blocks overwrites for the whole TTL, which limits the "overwrite a good record with a referral" pattern.
Bailiwick and glue hardening
Unbound's harden-glue (default yes) trusts glue only if it is within the server's authority, and harden-referral-path (default off, because of the extra load) validates NS sets and addresses along the referral path when zones are signed. After MaginotDNS, the most important change was patching the logic itself and, where possible, not running forwarding and recursion on the same server.
DNS Cookies (RFC 7873, RFC 9018)
RFC 7873 (May 2016) defines DNS Cookies, an EDNS option with a client cookie and a server cookie. The client cookie is 8 bytes and should have at least 64 bits of entropy; the server cookie is 8 to 32 bytes and is derived from a server secret and the client's address. The RFC's stated purpose is protection against "forgery or cache poisoning attacks by off-path attackers": a reply that does not echo the client's cookie can be dropped. RFC 9018 (April 2021) standardised how server cookies are built so that anycast servers from different vendors can verify each other's cookies.
Cookies only help when both sides use them. BIND sends cookies on outgoing queries by default (send-cookie yes) and can require them from clients (require-server-cookie, default no). Unbound answers cookies only if answer-cookie: yes is set; its default is no.
Minimal responses
Extra records in the authority and additional sections make replies bigger and give forged data more places to hide. Unbound's minimal-responses defaults to yes; BIND's option of the same name also offers no-auth and no-auth-recursive.
QNAME minimisation (RFC 9156)
RFC 9156 (November 2021, obsoleting RFC 7816) describes QNAME minimisation: the resolver sends only as many labels as each server needs, so the root sees com, not www.example.com. Its abstract is clear that this is a privacy measure. It is not a poisoning defence, and you should not count it as one. It does interact with 0x20, because minimised queries have fewer letters to randomise at upper levels, and its fallback logic matters for reliability. The RFC recommends a limit of 10 minimisation iterations. Unbound enables it by default (qname-minimisation: yes) and BIND's default is relaxed, which falls back to full queries on unexpected responses.
Fragmentation avoidance
Covered above: both main open source resolvers default to 1,232 bytes, retry over TCP when truncated, and RFC 9715 tells requestors to drop fragmented replies.
Comparison of resolver defences
| Defence | What it adds | Protects against | Default in BIND | Default in Unbound |
|---|---|---|---|---|
| Source port randomisation | Up to about 16 bits | Blind guessing, Kaminsky | Always on | Always on |
| 0x20 casing | 1 bit per letter | Blind guessing | No such option in the BIND reference | use-caps-for-id: no |
| DNS Cookies | 64 bit secret if both sides support | Off path forgery | send-cookie yes | answer-cookie: no (serving side) |
| 1,232 byte EDNS buffer | No fragments to forge | Fragmentation | 1,232 | 1,232 |
| Glue and bailiwick hardening | Refuses out of zone data | Kaminsky style referrals, logic bugs | Built in | harden-glue: yes |
| DNSSEC validation | Cryptographic check | Every forgery, for signed zones | dnssec-validation auto | Needs a trust anchor configured |
| Minimal responses | Smaller replies | Reduces surface and size | Option | minimal-responses: yes |
| QNAME minimisation | Privacy only | Not a poisoning defence | relaxed | qname-minimisation: yes |
How does encrypted DNS (DoT, DoH, DoQ) change the threat?
Three standards encrypt DNS:
- DNS over TLS (DoT), RFC 7858, May 2016: DNS messages over a TLS connection on TCP port 853.
- DNS over HTTPS (DoH), RFC 8484, October 2018: DNS messages carried in HTTPS requests with the media type
application/dns-message. - DNS over QUIC (DoQ), RFC 9250, May 2022: DNS over a dedicated QUIC connection on UDP port 853.
All three authenticate the resolver and encrypt the traffic, so an attacker on the path to your resolver can neither read nor forge answers on that hop, closing the last hop gap DNSSEC leaves when a stub trusts the AD bit.
They do not change the resolver's own exposure. The SAD DNS FAQ answers the question directly: DoH "only encrypts the traffic between the DNS client and DNS resolvers", while cache poisoning attacks target the link between resolvers and authoritative servers. Very few authoritative servers accept encrypted queries from resolvers today. If your resolver's cache is poisoned, encrypted DNS delivers the poisoned answer to you securely.
| Layer | Plain UDP or TCP | DoT, DoH, DoQ | DNSSEC validation at resolver | DNSSEC validation on the device |
|---|---|---|---|---|
| Device to resolver (on path attacker) | Exposed | Protected | Exposed unless the hop is protected | Protected for signed zones |
| Resolver to authoritative (off path poisoning) | Exposed to forgery | Not covered | Protected for signed zones | Protected for signed zones |
| Unsigned zones | Exposed | Device to resolver hop only | Not protected | Not protected |
On Windows, Microsoft documents DoH support in the DNS client starting with Windows Server 2022, available only when the configured DNS server is on the list of known DoH servers. On Linux with systemd-resolved, DNSOverTLS= takes yes or opportunistic, and its documentation notes that strict mode validates the server certificate against the name given in DNS= with the address#server_name form.
A sensible combination for most organisations: encrypt from devices to a resolver you control, have that resolver validate DNSSEC strictly, keep its software and kernel patched, and keep it single purpose.
How do you check your resolver's settings and DNSSEC status?
The steps below only read configuration and send ordinary queries. None of them change a running server. Run them against resolvers you operate. Replace 127.0.0.1 with your resolver's address.
Step 1: confirm the resolver validates DNSSEC
Query a zone you know is signed, for example iana.org, and look for the ad flag in the header.
dig @127.0.0.1 iana.org A +dnssec +multi | grep -E 'flags:|RRSIG'
# Expect "flags: qr rd ra ad" and RRSIG records.
# No "ad" flag means the resolver did not validate the answer.
Then use delv, BIND's validation tool, which performs its own validation from the root trust anchor and reports the result in words.
delv @127.0.0.1 iana.org A
# "; fully validated" means the chain checked out.
# "; unsigned answer" means the zone is not signed.
Check whether the domains you rely on for downloads are signed at all:
for d in deb.debian.org registry.npmjs.org pypi.org repo.maven.apache.org; do
printf '%s: ' "$d"; delv "$d" A 2>&1 | grep -Eo 'fully validated|unsigned answer|resolution failed' | head -1
done
The point is to know which of your critical names DNSSEC can protect and which rely on TLS and signatures alone.
Step 2: confirm the root trust anchor includes KSK-2024
For Unbound with an RFC 5011 managed anchor file (the path varies by distribution; check auto-trust-anchor-file in your config first):
unbound-checkconf -o auto-trust-anchor-file
sudo grep -E 'DNSKEY.*257' /var/lib/unbound/root.key
sudo unbound-anchor -v -a /var/lib/unbound/root.key # reports the anchor state; run as the unbound user if required
For BIND with dnssec-validation auto, the root anchor is built in and maintained by the server. Check the managed keys state:
sudo rndc managed-keys status
# Look for key tag 38696 listed as trusted.
With any validating resolver, you can also check from the outside:
dig @127.0.0.1 . DNSKEY +multi | grep -E 'key id = (38696|20326)'
On Windows DNS Server, in an elevated PowerShell:
Get-DnsServerTrustAnchor -Name "."
Get-DnsServerSetting -All | Select-Object EnableDnsSec
Step 3: check the hardening settings on Unbound
Print the effective value of each option. unbound-checkconf -o reads the configuration without starting a server.
for o in use-caps-for-id harden-glue harden-dnssec-stripped harden-referral-path \
qname-minimisation aggressive-nsec minimal-responses edns-buffer-size max-udp-size \
answer-cookie val-permissive-mode unwanted-reply-threshold outgoing-range; do
printf '%-26s %s\n' "$o" "$(unbound-checkconf -o $o)"
done
A hardened recursive configuration might look like this. Test it on a staging resolver first: use-caps-for-id can cause failures for zones whose servers do not echo case, which is what caps-exempt is for.
server:
# Validation
auto-trust-anchor-file: "/var/lib/unbound/root.key"
harden-dnssec-stripped: yes
val-permissive-mode: no
harden-algo-downgrade: yes
# Referral and glue handling
harden-glue: yes
# Extra entropy against blind forgery
use-caps-for-id: yes
# caps-exempt: "example-zone-that-breaks.test"
# Fragmentation avoidance (these are the defaults, stated explicitly)
edns-buffer-size: 1232
max-udp-size: 1232
# Smaller answers, privacy
minimal-responses: yes
qname-minimisation: yes
aggressive-nsec: yes
# Flush caches if a flood of unmatched replies suggests an attack
unwanted-reply-threshold: 10000000
Unbound's documentation describes unwanted-reply-threshold as a defensive action that clears the RRset and message caches when too many unmatched replies arrive, "hopefully flushing away any poison", and suggests 10 million.
Validate the file and reload:
sudo unbound-checkconf
sudo unbound-control reload
Step 4: check BIND
named-checkconf -p prints the parsed configuration, which shows what you set; options you did not set use the defaults described in the BIND reference.
sudo named-checkconf -p | grep -E 'dnssec-validation|qname-minimization|minimal-responses|send-cookie|require-server-cookie|edns-udp-size|max-udp-size|forward|forwarders|query-source'
named -V | head -1
If the output shows both forwarders and recursion on the same server, read the next section on MaginotDNS and make sure the version is at or above the fixed releases for your branch. A conservative options block:
options {
recursion yes;
allow-recursion { 10.0.0.0/8; 192.168.0.0/16; }; // never an open resolver
dnssec-validation auto;
minimal-responses no-auth-recursive;
qname-minimization relaxed;
send-cookie yes;
edns-udp-size 1232;
max-udp-size 1232;
};
Check, reload and confirm:
sudo named-checkconf && sudo rndc reload && rndc status
Step 5: check Windows DNS Server
In an elevated PowerShell on the DNS server:
# Socket pool size and excluded ranges
Get-DnsServerSetting -All | Select-Object SocketPoolSize, SocketPoolExcludedPortRanges, EnableDnsSec
# Cache locking (100 means never overwrite a cached record during its TTL)
Get-DnsServerCache | Select-Object LockingPercent, MaxTtl, MaxNegativeTtl
# Test a signed name through this server, asking for DNSSEC records
Resolve-DnsName iana.org -Server 127.0.0.1 -DnssecOk
If LockingPercent has been lowered from 100, find out why before you leave it that way.
Step 6: check the client side on Linux
On systems with systemd-resolved:
resolvectl status | grep -E 'DNSSEC|DNSOverTLS|Current DNS Server'
resolvectl query iana.org # reports "authenticated: yes" when validation succeeded
allow-downgrade turns validation off when the upstream server looks DNSSEC unaware; that matters most when the upstream hop is neither encrypted nor validated.
Step 7: check the kernel and the network path for SAD DNS
On Linux resolvers, confirm the kernel has the randomised ICMP rate limit by checking that you run a maintained kernel at or above your distribution's fix for CVE-2020-25705:
uname -r
# Compare against your distribution's security tracker entry for CVE-2020-25705 and CVE-2021-20322.
If you want to remove the side channel entirely, drop outgoing ICMP port unreachable messages from the resolver host. Test this on a staging host first; it changes what remote peers see when they hit closed ports.
# nftables example: drop outgoing ICMP and ICMPv6 port unreachable from this host
sudo nft add table inet resolver_guard
sudo nft add chain inet resolver_guard out '{ type filter hook output priority 0; }'
sudo nft add rule inet resolver_guard out icmp type destination-unreachable icmp code port-unreachable drop
sudo nft add rule inet resolver_guard out icmpv6 type destination-unreachable icmpv6 code port-unreachable drop
Step 8: watch for the signs
The SAD DNS FAQ lists what an intrusion detection system might notice: periodic bursts of packets (their attack sent a burst every 50 ms), UDP port scanning of the resolver, and DNS replies with wrong transaction IDs, which normal traffic almost never contains. On Unbound, unbound-control stats_noreset | grep -i unwanted shows the unwanted query and reply counters; a sustained rise is worth investigating.
Why do software updates and package downloads depend on DNS integrity?
Every package install starts with a name lookup: a distribution mirror, a package registry, a container registry, an update server, a CI runner fetching a toolchain. If that lookup is poisoned, the client connects to the attacker, and what happens next depends entirely on what the client checks after DNS.
- HTTPS with certificate validation means a poisoned lookup alone is not enough. The attacker also needs a certificate the client trusts for that name. Poisoning becomes a denial of service or a phishing setup, not silent substitution, unless certificate checks are disabled or a CA can be fooled. Certificate issuance itself often relies on DNS based domain validation, which is one reason resolver integrity at certificate authorities matters.
- Signed repository metadata survives a poisoned lookup even over plain HTTP. Debian style repositories sign their release files, and clients refuse unsigned or wrongly signed indexes. The Update Framework (TUF) specification lists the attacks a well designed update system should resist, including "arbitrary installation attacks", rollback attacks and mix and match attacks, all of which assume the network may be hostile.
- Package signatures and hashes pin what you install to what you expected. npm documents ECDSA registry signatures that
npm audit signaturescan verify. pip's hash checking mode (--require-hashes) refuses any download whose hash does not match the requirements file. Lockfiles with integrity hashes do the same for npm, Yarn and pnpm. - Things that check nothing are exposed. Install scripts that pipe a URL into a shell, build steps that download a binary over HTTP without a checksum, internal mirrors on plain HTTP with no signed metadata, and update clients that ignore certificate errors all turn a poisoned DNS answer into code execution.
Our earlier post on DNS cache poisoning against software update channels and the broader cache poisoning prevention guide go further into update channel specifics. For how attackers get on path in the first place, see how attackers intercept network traffic over copper, fiber and Wi-Fi.
A short checklist for build and update paths:
# npm: verify registry signatures for installed packages
npm audit signatures
# pip: install only artefacts whose hashes match the lock
pip install --require-hashes -r requirements.txt
# apt: confirm no repository is marked trusted=yes (which disables signature checks)
grep -R "trusted=yes" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null
# CI: find downloads piped straight into a shell
grep -RInE 'curl[^|]*\|\s*(sudo\s+)?(ba)?sh|wget[^|]*\|\s*(sudo\s+)?(ba)?sh' .github/ scripts/ 2>/dev/null
Common mistakes
- Treating encrypted DNS as poisoning protection. DoT, DoH and DoQ protect the device to resolver hop. A poisoned resolver cache is delivered securely.
- Assuming the DO bit means validation. BIND sets DO even with validation off. Check for the
adflag or afully validatedresult fromdelv. - Leaving validation in a permissive or downgradable mode.
val-permissive-mode: yeson Unbound andDNSSEC=allow-downgradeon systemd-resolved both let bogus or unvalidated data through in some conditions. - Forgetting the trust anchor. After the October 2026 root rollover, a validating resolver without KSK-2024 will fail every lookup. ICANN explicitly says not to assume automatic updates worked.
- Running forwarding and recursion on one server without thinking about it. That is the MaginotDNS setup. If you must, patch and keep the software current.
- Raising the EDNS buffer to "fix" truncation. Truncation with TCP fallback is the safe behaviour. A 4,096 byte buffer invites fragmentation.
- Blocking TCP 53 at the firewall. Without TCP, a resolver with a 1,232 byte buffer cannot fetch large signed answers, and operators then raise the buffer, which brings fragmentation back.
- Pinning the query source port for a firewall rule. It undoes the 2008 fix. Allow the resolver's ephemeral range outbound instead.
- Patching the resolver but not the kernel. SAD DNS lived in the network stack, not the DNS software.
- Relying on DNS alone for update integrity. If a build step downloads code without TLS, signatures or hashes, DNS is the only thing between you and arbitrary code.
FAQ
What is DNS cache poisoning in simple terms?
It is getting a DNS resolver to store a false answer, such as the wrong IP address for a bank or a package registry. Every user of that resolver then receives the false answer until the record expires or the cache is flushed. The attacker does not need to touch the users' devices or the real domain.
Is DNS cache poisoning still possible in 2026?
Yes, but it is much harder than before 2008 and depends on finding a way around current defences: a side channel that leaks the port, a fragmentation path, an unpatched logic bug, or a resolver with old software or a weak configuration. For signed zones queried through a strictly validating resolver, a forged answer is rejected.
What was the Kaminsky DNS bug?
CVE-2008-1447, disclosed on 8 July 2008. Lookups for nonexistent names gave an off path attacker unlimited races, and forged in bailiwick referral data handed it the whole domain. The fix was per query source port randomisation.
How does source port randomisation stop DNS spoofing?
It adds a second unknown: which of tens of thousands of ports the query used. RFC 5452's figures move the time to a 50% chance from about 7 seconds to about 116 hours.
What is SAD DNS, and is it fixed?
A 2020 attack that used the operating system's global ICMP rate limit to discover a resolver's source port. Linux randomised that limit (CVE-2020-25705, in 5.10 and stable backports); blocking outgoing ICMP unreachables, cookies, 0x20 and DNSSEC also help.
What is MaginotDNS?
A 2023 attack on servers that both forward and recurse, exploiting inconsistent bailiwick checks between the two roles. For BIND it is CVE-2021-25220, fixed in 9.11.37, 9.16.27 and 9.18.1.
Does DNS over HTTPS prevent DNS cache poisoning?
No. DoH protects the device to resolver hop; poisoning targets the resolver's queries to authoritative servers. DoH does stop a local network attacker forging answers to you.
Does DNSSEC stop cache poisoning?
For signed zones, through a resolver that validates strictly, yes. It does nothing for unsigned zones or non validating resolvers.
How do I check if my DNS resolver validates DNSSEC?
Query a signed name such as iana.org with dig +dnssec and look for the ad flag, or run delv and look for fully validated. On systemd-resolved, resolvectl query reports authenticated: yes. Also confirm the root trust anchor includes key tag 38696.
What is DNS 0x20 encoding?
Randomly mixing letter case in a query name and requiring the reply to echo it. Each letter adds one bit to guess. It comes from a 2008 Internet Draft and is optional in Unbound.
Where Safeguard fits, honestly
To be clear about scope: Safeguard does not run or monitor DNS resolvers, inspect network traffic or detect poisoning. Where it helps is the software side of this history. It scans repositories, container images and SBOM files for components such as resolver packages, kernels and package manager tooling, matches them against vulnerability data, and shows which projects contain a given component and version, so a resolver advisory becomes a list of affected builds instead of a hunt.
DNS integrity is a network property, but the software that resolves, validates and fetches is ordinary open source that ships inside your images and appliances: resolver daemons, the C library's stub resolver, systemd-resolved, kernels, and the package managers that trust what DNS returns. When a resolver advisory like CVE-2021-25220 lands, the hard part is usually finding every image and service that carries the affected version, which is the supply chain hygiene the next section covers.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
This demo is a real Safeguard CLI session. The assistant reads a finding and its advisory, asks before each command, edits a manifest to the fixed version, and runs the install and tests with your approval. The same flow applies when an image you build carries a resolver library or tool at a version named in an advisory: you ask, review each step and push nothing until you are ready.
Click through it at your own pace, or open it full screen.
The local runner demo shows setting up a runner on your own machine, approving it with its device code and scanning from the terminal. For DNS infrastructure that is useful when the images that run your resolvers or build agents live on internal systems you would rather scan in place.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. Once connected, you can ask in plain language which of your projects carry a particular resolver or library version and get the answer from your own scan data.
Click through it at your own pace, or open it full screen.
You can download the apps at safeguard.sh/download and read the CLI documentation at docs.safeguard.sh/docs/cli.
Quick check
Why did source port randomisation not end the cache poisoning story?
Because later attacks went around the port instead of guessing it. Fragmentation attacks replaced a reply's second fragment, which carries no DNS secrets, and SAD DNS used a global ICMP rate limit to discover the port, leaving only the 16 bit ID to guess.
Does turning on DNS over HTTPS on laptops protect a company resolver's cache?
No. DoH protects the hop from the laptop to the resolver. Poisoning targets the resolver's queries to authoritative servers. Protect the cache with patched software and kernels, strict DNSSEC validation, small EDNS buffers with TCP fallback, and a single purpose resolver.
Sources
- CERT/CC Vulnerability Note VU#800113: Multiple DNS implementations vulnerable to cache poisoning
- NVD: CVE-2008-1447
- Microsoft Security Bulletin MS08-037
- RFC 1035: Domain names, implementation and specification
- RFC 5452: Measures for Making DNS More Resilient against Forged Answers
- RFC 4033: DNS Security Introduction and Requirements
- RFC 7873: Domain Name System (DNS) Cookies
- RFC 9018: Interoperable Domain Name System (DNS) Server Cookies
- RFC 9156: DNS Query Name Minimisation to Improve Privacy
- RFC 7858: DNS over Transport Layer Security
- RFC 8484: DNS Queries over HTTPS
- RFC 9250: DNS over Dedicated QUIC Connections
- RFC 9715: IP Fragmentation Avoidance in DNS over UDP
- Use of Bit 0x20 in DNS Labels to Improve Transaction Identity (Internet Draft, 2008)
- Herzberg and Shulman, Fragmentation Considered Poisonous (arXiv 1205.4011)
- DNS Flag Day 2020
- SAD DNS project site, UC Riverside
- NVD: CVE-2020-25705
- USENIX Security 2023: The Maginot Line: Attacking the Boundary of DNS Caching Protection
- MaginotDNS paper (PDF)
- ISC: CVE-2021-25220, DNS forwarders cache poisoning vulnerability
- NVD: CVE-2021-25220
- ICANN: Root Zone KSK Rollover
- IANA: DNSSEC information
- IANA: Root zone trust anchors (root-anchors.xml)
- Unbound: unbound.conf reference
- BIND 9 configuration reference
- BIND 9 manual pages (delv, dig)
- Microsoft Learn: Set-DnsServerCache
- Microsoft Learn: Get-DnsServerSetting
- Microsoft Learn: Get-DnsServerTrustAnchor
- Microsoft Learn: Resolve-DnsName
- Microsoft Learn: DNS over HTTPS client support in Windows Server
- systemd: resolved.conf
- The Update Framework specification
- pip: Secure installs
- npm Docs: About ECDSA registry signatures
Last reviewed: 24 October 2026. Resolver defaults and kernel fixes change between releases; check the linked RFCs, advisories and documentation for your versions.