DEV Community

Cover image for Path Consistency in Kubernetes DNS Stacks: Do Resolvers Agree Under Adversarial Conditions?
Kazuru
Kazuru

Posted on

Path Consistency in Kubernetes DNS Stacks: Do Resolvers Agree Under Adversarial Conditions?

Most Kubernetes nodes do not have one DNS. They have a path: pod stub / libc, often systemd-resolved on the host, sometimes NodeLocal DNSCache, then CoreDNS, then upstream. The security question that matters to operators is not which recursive binary won a paper bake-off. It is:

Does hop N on my path accept something hop N+1 would reject?

That is path-consistency. Existing differential fuzzers mostly answer a different question (peer-consistency). This post introduces StackDiff: a lab harness for the path question, with an honest scope about what plain containers can and cannot measure.

This is a methodology post. No findings tables, no CVE counts, no Klein/SAD reproduction, and not a claim that a full Kubernetes path is already instrumented.

Path vs peer

Peer-consistency (ResolFuzz / ResolverFuzz) vs path-consistency (StackDiff)

Effective cluster posture is the weakest agreeing hop, not the strongest row in a comparison table.

Threat model

Every profile declares three fields before it counts:

Field Meaning
Adversary position on-path / off-path / malicious-but-trusted-upstream / none (smoke)
Adversary capability What they can manipulate: response content, timing, races, ICMP, and so on
Win condition What success means here, for example one hop caches what another rejects

Every profile also pins DNSSEC posture (matched vs deliberately_mismatched):

"dnssec_posture": {
  "mode": "matched",
  "unbound": "off-lab-zone",
  "dnsmasq": "off",
  "notes": "Matched non-validating so Unbound defaults cannot manufacture a divergence"
}
Enter fullscreen mode Exit fullscreen mode

Unbound validates by default; dnsmasq does not unless configured. An unpinned mismatch manufactures fake Class A “findings” that are really config accidents.

Path-consistency win: disagreement along the path so the cluster’s effective posture becomes the weakest agreeing hop. Smoke profile P-SMOKE-AGREE uses position none and matched non-validating DNSSEC. Divergence on the smoke axes is a harness failure, not a win.

Scope / isolation

Containers share a kernel. That matters for some DNS attacks and not others.

Klein targets the Linux kernel’s shared prandom generator (UDP source ports, IPv6 flow labels, IPv4 ID). The paper (arXiv:2012.07432) shows the attack can run locally across users and across containers on the same host. Shared-kernel Docker does not isolate it. Klein-class profiles are deferred to Firecracker / Kata / full VMs.

SAD DNS (CVE-2020-25705) uses ICMP rate limiting as a side channel. Two kernel changes matter, and they are not the same thing:

  1. Primary mitigation: Linux randomized the ICMP global rate limiter (b38e7819cae946e2edf869e604af1e65a5d241c5), mainline 5.10+ with wide LTS backports.
  2. Isolation improvement: msgs-per-sec token state moved from host-wide to per-netns between v6.11 and v6.12 (b056b4cd9178f7a1d5d57f7b48b073c29729ddaa). That changes whether two containers share the bucket, not whether the side channel was already noised.
Layer Plain Docker? This release
Application-layer outputs (RCODE, RRset, flags, hang/crash) Valid In scope
Klein-class shared prandom Not isolated Deferred
SAD DNS-class ICMP side channel Randomization ≠ netns isolation Deferred until kernel pin

Ubuntu 24.04 and many enterprise kernels are often still v6.11-shaped for the msgs-per-sec bucket. Always record uname -r and the distro package. Prefer full commit URLs.

Related work

Tool Venue Question
ResolFuzz (Bushart & Rossow) ESORICS 2023 Do recursive resolvers diverge on mutated inputs?
ResolverFuzz (Zhang et al.) USENIX Security 2024 Query–response fuzzing and differential clustering; real cache-poisoning-class bugs with CVE follow-through

StackDiff does not replace either tool. It asks a path-consistency question under pinned application-layer profiles.

Architecture (pair mode)

The public pin tests resolver pairs under identical conditions so the oracle can be trusted before anyone treats divergences as security-relevant. A full Kubernetes path (stub → node-local → CoreDNS → upstream) is a different measurement unit. Pair mode asks whether two path-role stand-ins agree; it does not claim the whole chain is instrumented.

Unbound and dnsmasq are those stand-ins: dnsmasq approximates local forwarder/cache behavior (NodeLocal DNSCache / systemd-resolved); Unbound approximates a recursive hop. Smoke keeps Unbound forward-only against the same lab authority as dnsmasq, with matched non-validating DNSSEC.

Class Meaning Rule
A Documented / RFC-allowed disagreement Report; do not hype
B Emergent, security-relevant Root-cause and disclosure first
C Timing, nondeterminism, harness bug Fix harness; never publish as finding

Smoke gate

Both resolvers must return RCODE=NOERROR, the same answer RRset containing 203.0.113.10 for agree.lab.stackdiff. A, and neither side may hang or hard-error. Any mismatch on those axes is Class C / stop. AA/RA are informational only.

Profile Resolvers RCODE RRset Hang/crash Class
P-SMOKE-AGREE Unbound ↔ dnsmasq agree agree none pass

On this forward-only path, dnsmasq often sets aa=true while Unbound sets aa=false for the same answers. That is expected noise, not a smoke failure.

Lab pin

Field Value
Repo dns-stack-diff-harness
Tag blog-dns01-2026-07
Host Ubuntu 24.04.4 LTS (stackdiff-lab), QEMU/KVM
uname -r 6.8.0-134-generic
Kernel package linux-image-6.8.0-134-generic 6.8.0-134.134
Docker / Compose 29.1.3 / 2.40.3
Run smoke-20260718T125325Z
pass / divergences true / 0
Manifest artifacts/smoke-20260718T125325Z/manifest.json
Manifest SHA-256 6804627105cd22b51b35e9df1c713f2fe26c5c4d67abb81bfdd2064be99e0560

This host is v6.11-shaped for the ICMP msgs-per-sec bucket. Do not claim per-netns isolation here. Cite the pin (uname -r and the committed manifest SHA-256), not main plus whatever Docker Desktop shipped. The published uname_a uses the public alias stackdiff-lab.

Reproduce

git clone https://github.com/kazuru-chidumbwe/dns-stack-diff-harness
cd dns-stack-diff-harness
git checkout blog-dns01-2026-07
# Verify the frozen pin (exact hash of the committed file):
sha256sum artifacts/smoke-20260718T125325Z/manifest.json
# expect: 6804627105cd22b51b35e9df1c713f2fe26c5c4d67abb81bfdd2064be99e0560

docker compose -f deploy/compose.yaml up -d --build
make smoke
# Your new artifacts/smoke-* run will have a different SHA (timestamps differ).
# What should match the pin: pass=true and oracle.divergence_count=0
Enter fullscreen mode Exit fullscreen mode

Needs Docker Compose and Python 3.12+. Lab DNS only.

What this post is not claiming

  • New CVEs or “N exploitable divergences”
  • Klein under Docker, or SAD DNS without a pinned kernel story
  • A replacement for ResolFuzz or ResolverFuzz
  • That pair mode already is a full stub → node-local → CoreDNS path

It claims a path-consistency instrument (pair-validated first), an explicit isolation boundary, and a smoke gate that is green on the pin above. The harness is public on that pin; this post is methodology only. If you want harness updates, watch dns-stack-diff-harness on GitHub.

Top comments (0)