From ce7819a7da3fe03163970584a13d6bdb2349f0ca Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Mon, 14 Sep 2026 00:11:30 -0500 Subject: [PATCH] =?UTF-8?q?docs:=20FINDING=20=E2=80=94=20public=20DNS=20pu?= =?UTF-8?q?blishes=20internal=20RFC1918=20addresses,=20contradicting=20R-D?= =?UTF-8?q?NS-03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-reported, verified. external-dns runs provider cloudflare with gateway-httproute + service sources and publishes the Cilium Gateway VIP 192.168.2.20, so ~16 hostnames in a public authoritative zone answer with RFC1918 space. The spec says the opposite: external resolves to the public IP, Pi-hole serves the internal split-horizon answers, internal services are unreachable from the internet. Provenance stated plainly: the pattern predates me — external-dns always published the Gateway address, and dev did the same under owner=default. My apex migration changed which names carry it, propagating an existing flaw onto the apex rather than inventing it. Four consequences recorded: topology and service-inventory disclosure; a DNS-rebind primitive, which is why the SOHO router strips these answers and LAN clients need per-client overrides; the five services declaring exposure public are not actually publicly reachable; and seven internal services are publicly advertised. Also records why deleting the records is not a safe first step: the *.home.keithjackson.dev wildcard is gone, so removal yields NXDOMAIN rather than a fallback. Remediation sequenced, nothing started. Co-Authored-By: Claude Opus 5 --- DECISIONS-LOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/DECISIONS-LOG.md b/DECISIONS-LOG.md index 5db94a1..81c9cd4 100644 --- a/DECISIONS-LOG.md +++ b/DECISIONS-LOG.md @@ -3858,3 +3858,65 @@ Two genuine discoveries from that re-measurement: queueing, which is also why its queue time stays near zero. This is an independent signal that prod-1 sustains far more concurrent NFS work than the other two nodes, and it is consistent with three of the four NFS-heavy pods being scheduled there. + +--- + +## 2026-09-14 — FINDING (operator): public DNS carries internal RFC1918 addresses + +The operator pointed out that public DNS records resolve to internal, non-routable IPs. Verified, and +it is worse than disclosure alone. + +**What is published.** `platform/externaldns/helmrelease.yaml` runs `provider: cloudflare` with +`sources: [gateway-httproute, service]` and `policy: sync`. external-dns faithfully publishes each +source's address, and that address is the Cilium Gateway's LoadBalancer VIP `192.168.2.20` (plus the +L4 service IPs `.21` unifi and `.22` crafty-mc). So ~16 hostnames in a **public authoritative zone** +answer with RFC1918 space. + +**This contradicts the spec.** `docs/09-dns-and-ingress.md` R-DNS-03 states: *"**External** +(`*.home.keithjackson.dev` from the internet): resolves to the **public IP**"*, *"**PiHole** … +provides internal LAN DNS and the split-horizon answers"*, and *"`internal` services are unreachable +from the public internet"*. The deployed arrangement inverts it — the internal VIP is what the world +is told, and there is no internal authority at all (Pi-hole serves no DNS; see the entry above). + +**Provenance, plainly.** The pattern is architectural and predates me: external-dns has always +published the Gateway's address to Cloudflare, and on dev it did the same with dev's internal VIP +under `owner=default`. What MY apex migration changed is *which* names carry it — prod moved from +`prod.home.keithjackson.dev` to the apex, so `home.keithjackson.dev` and its subdomains now publish +internal addresses where previously they were dev's records or the wildcard. I propagated an existing +flaw onto the apex; I did not invent it. I also widened prod's `EXTERNALDNS_DOMAIN_FILTER` from its +own subtree to the zone apex `keithjackson.dev` per finding F-DNS1 (a strict-subdomain filter drops +the parent zone), which is the documented fallback but leaves `txtOwnerId` as the sole ownership guard. + +**Four consequences:** +1. **Information disclosure.** Anyone can enumerate the internal subnet, the Gateway VIP, both L4 + service addresses, and the full service inventory — sonarr, radarr, qbittorrent, jackett, gitea, + nextcloud, keycloak, semaphore, pihole, plex, seerr, crafty, headlamp, unifi, runbooks, grafana. + No credentials leak, but topology and a target list do. +2. **A DNS-rebind primitive.** A public name answering with a private address is exactly the rebind + shape browsers and resolvers defend against — which is *why* the SOHO router at 192.168.2.1 strips + these answers, and therefore why LAN clients need per-client dnsmasq overrides. The security + control is fighting the misconfiguration. +3. **The five `public` services are not actually publicly reachable.** plex, gitea, seerr, keycloak and + nextcloud declare `exposure: public`, yet their records point at 192.168.2.20, which no internet + client can route to. The declaration is unsatisfied. +4. **Seven `internal` services are advertised publicly** — pihole, radarr, sonarr, jackett, vm-demo, + semaphore, qbittorrent — which the spec says must be unreachable from the internet. They are + unreachable, but they should not be *listed*. + +**Removing the records is NOT a safe first step.** The `*.home.keithjackson.dev` wildcard that used to +answer for this subtree is **gone** — `nothing-here.home.keithjackson.dev` returns NXDOMAIN — so +deleting the specific records yields no answer at all rather than falling back. Only +`*.keithjackson.dev` still resolves, to the legacy public IP `136.51.115.252` (ports 80/443 open, +serving a placeholder certificate `CN=%D`, so the legacy stack's TLS is not configured for these +names either). + +**Sequenced remediation — NOT started, needs the operator's decisions:** +1. **Stand up internal DNS first.** Expose Pi-hole's DNS on port 53 via a LoadBalancer IP and make it + authoritative for the zone internally. Until this exists, removing public records breaks LAN + resolution outright, because the router strips RFC1918 and nothing else answers. +2. **Then stop publishing internal names publicly.** Either scope external-dns to only the services + declared `public`, or point it at an internal provider, or drop the Cloudflare provider for this + subtree entirely. This is the step that closes the disclosure. +3. **Then decide what `public` means for the five.** If they are genuinely meant to be + internet-reachable, their records must target the public IP with edge port-forwards and real + certificates; if not, they should be re-declared `internal`. Today they are neither.