docs: FINDING — public DNS publishes internal RFC1918 addresses, contradicting R-DNS-03
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:
1 parent
3de79f81a5
commit
ce7819a7da
1 file changed
+62
@@ -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.
|
||||
Reference in new issue
Block a user