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.