JIT
← Notebook
Field notes DNSSecurityNetworkingHomelab

Wanneer je gateway DNS kaapt

Een interne site die niet laadde, een resolver die niemand bevroeg, en de dead-IP-query die bewees dat een 'behulpzame' gateway stiekem mijn DNS beantwoordde.

Een interne site was opeens vanaf geen enkele client meer bereikbaar. De stack erachter was prima — poort 443 reageerde, en curl --resolve gaf netjes HTTP 200 met een geldig certificaat. De server was dus niet het probleem; niemand kon hem vinden. Klassiek DNS — alleen waren alle voor de hand liggende verdachten onschuldig.

De resolver werd niet eens bevraagd

Interne namen leven in een split-horizon-zone op een resolver die ik zelf draai; publieke namen gaan naar het internet. Toen ik die resolver vanaf een client bevroeg, kreeg ik een leeg antwoord — en de authority-sectie was ondertekend door de publieke nameservers van mijn domein, niet door mijn interne resolver. Dat kán niet als de query mijn resolver echt had bereikt.

Dus richtte ik dig op een gegarandeerd dood adres — 192.0.2.1, een documentatie-IP dat niets kan beantwoorden — op poort 53. Het antwoordde. Dat is het hele bewijs: als een dood IP een DNS-query beantwoordt, dan onderschept iets op het pad poort 53. De gateway DNAT’te stilletjes alle DNS van elke client naar zijn eigen resolver, die doorstuurde naar het publieke internet — waar interne namen simpelweg niet bestaan. Geen IPv6, niet de resolver die iedereen verdacht. Een ‘behulpzame’ DNS-functie van de gateway die split-horizon per definitie sloopt.

Eén resolver, alle lagen

De fix bestond uit twee stappen: de gateway laten stoppen met het onderscheppen van DNS, en elke laag DNS-beveiliging onderbrengen in de ene resolver die ik echt beheer en kan inspecteren.

<rect x="230" y="20" width="300" height="48" rx="12" fill="#eef0f8" stroke="#3b4eab" stroke-width="2" />
<text x="380" y="41" text-anchor="middle" font-size="15" font-weight="700" fill="#1f2752">Client-query</text>
<text x="380" y="59" text-anchor="middle" font-size="11" fill="#64748b">elk apparaat → precies één resolver</text>
<line x1="380" y1="68" x2="380" y2="88" stroke="#8b96cd" stroke-width="2" marker-end="url(#ah)" />

<rect x="180" y="90" width="400" height="72" rx="12" fill="#f5f6fc" stroke="#3b4eab" stroke-width="2" />
<text x="380" y="115" text-anchor="middle" font-size="14.5" font-weight="700" fill="#1f2752">Technitium — de resolver die ik beheer</text>
<text x="380" y="134" text-anchor="middle" font-size="11" font-family="ui-monospace, monospace" fill="#475569">split-horizon-autoriteit · filtering · recursieve forwarder</text>
<text x="380" y="152" text-anchor="middle" font-size="10.5" fill="#64748b">de gateway onderschept poort 53 niet meer</text>
<line x1="380" y1="162" x2="380" y2="186" stroke="#8b96cd" stroke-width="2" marker-end="url(#ah)" />

<!-- Stap A: split-horizon -->
<rect x="230" y="188" width="300" height="56" rx="10" fill="#fff" stroke="#3b4eab" stroke-width="1.5" />
<text x="380" y="212" text-anchor="middle" font-size="13" font-weight="700" fill="#1f2752">Split-horizon-check</text>
<text x="380" y="230" text-anchor="middle" font-size="10.5" fill="#64748b">is dit een van onze interne namen?</text>
<rect x="20" y="188" width="185" height="56" rx="10" fill="#eef0f8" stroke="#3b4eab" stroke-width="1.5" />
<text x="112" y="211" text-anchor="middle" font-size="11.5" font-weight="700" fill="#323f8a">Interne naam</text>
<text x="112" y="228" text-anchor="middle" font-size="10" fill="#475569">→ autoritatief antwoord</text>
<text x="221" y="210" text-anchor="middle" font-size="9" fill="#64748b">ja</text>
<line x1="230" y1="216" x2="208" y2="216" stroke="#8b96cd" stroke-width="1.5" marker-end="url(#ah)" />
<line x1="380" y1="244" x2="380" y2="268" stroke="#8b96cd" stroke-width="2" marker-end="url(#ah)" />

<!-- Stap B: blokkeerlijsten -->
<rect x="230" y="270" width="300" height="56" rx="10" fill="#fff" stroke="#3b4eab" stroke-width="1.5" />
<text x="380" y="294" text-anchor="middle" font-size="13" font-weight="700" fill="#1f2752">Advertentie- + dreigingslijsten</text>
<text x="380" y="312" text-anchor="middle" font-size="10.5" font-family="ui-monospace, monospace" fill="#475569">OISD Big · Hagezi TIF (~2M domeinen)</text>
<rect x="555" y="270" width="185" height="56" rx="10" fill="#fdecee" stroke="#dc2d3d" stroke-width="1.5" />
<text x="647" y="293" text-anchor="middle" font-size="11.5" font-weight="700" fill="#9d1926">Op een lijst</text>
<text x="647" y="310" text-anchor="middle" font-size="10" fill="#9d1926">→ NXDOMAIN (geblokkeerd)</text>
<text x="543" y="292" text-anchor="middle" font-size="9" fill="#9d1926">treffer</text>
<line x1="530" y1="298" x2="553" y2="298" stroke="#dc2d3d" stroke-width="1.5" marker-end="url(#ar)" />
<line x1="380" y1="326" x2="380" y2="350" stroke="#8b96cd" stroke-width="2" marker-end="url(#ah)" />

<!-- Stap C: hardening -->
<rect x="230" y="352" width="300" height="56" rx="10" fill="#fafbfd" stroke="#3b4eab" stroke-width="1.5" />
<text x="380" y="375" text-anchor="middle" font-size="12" font-weight="700" fill="#1f2752">DNSSEC · QNAME-min · geen EDNS-subnet</text>
<text x="380" y="393" text-anchor="middle" font-size="10" fill="#64748b">valideren · minimaliseren · client niet lekken</text>
<line x1="380" y1="408" x2="380" y2="432" stroke="#8b96cd" stroke-width="2" marker-end="url(#ah)" />

<!-- Stap D: versleutelde recursie -->
<rect x="230" y="434" width="300" height="56" rx="10" fill="#f5f6fc" stroke="#3b4eab" stroke-width="1.5" />
<text x="380" y="457" text-anchor="middle" font-size="13" font-weight="700" fill="#1f2752">Versleutelde DoT-recursie</text>
<text x="380" y="475" text-anchor="middle" font-size="10.5" font-family="ui-monospace, monospace" fill="#475569">DNS-over-TLS → Quad9 · Mullvad</text>
<line x1="380" y1="490" x2="380" y2="514" stroke="#8b96cd" stroke-width="2" marker-end="url(#ah)" />

<rect x="230" y="516" width="300" height="48" rx="12" fill="#eef0f8" stroke="#323f8a" stroke-width="2" />
<text x="380" y="538" text-anchor="middle" font-size="13.5" font-weight="700" fill="#1f2752">Gevalideerd antwoord → client</text>
<text x="380" y="556" text-anchor="middle" font-size="10" fill="#64748b">publieke namen, veilig opgelost</text>
Elke query komt binnen bij één resolver en doorloopt dezelfde pijplijn: interne namen krijgen een autoritatief antwoord, geblokkeerde namen sterven als NXDOMAIN (rood), de rest wordt DNSSEC-gevalideerd en gerecursed over versleutelde DoT. Blauw = normale flow, rood = geblokkeerd.

Technitium doet nu alles: autoritatieve antwoorden voor interne namen, advertentie- en dreigingsblokkering via OISD Big en Hagezi’s Threat Intelligence Feed (~2M domeinen, als NXDOMAIN), DNSSEC-validatie, QNAME-minimalisatie, EDNS Client Subnet weggelaten voor privacy, en recursie voor de rest over versleutelde DNS-over-TLS naar Quad9 en Mullvad. Alles staat in een Ansible-rol, dus het bouwt op uit code in plaats van uit het geheugen.

Split-horizon-DNS werkt alleen als je clients je resolver ook echt kunnen bereiken. Alles op het pad dat ‘behulpzaam’ poort 53 beantwoordt, is een stille single point of failure.

De les die ik onthoud: als DNS zich misdraagt, controleer dan niet alleen de server — controleer of je query er überhaupt aankomt. Een dead-IP-test kost tien seconden en vertelt je wie er echt antwoordt.

(Eerdere field notes uit deze hoek van de lab: de interne DNS in eerste instantie opzetten en een AI-agent Technitium leren spreken.)