docs: FINDING — public DNS publishes internal RFC1918 addresses, contradicting R-DNS-03
gate / gate (push) Has been cancelled
gate / conformance (push) Has been cancelled

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 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5 committed 2026-09-14 00:11:30 -05:00
1 parent 3de79f81a5
commit ce7819a7da
1 file changed
+62
+62
View File
@@ -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 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 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. 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.