Public names are proxied CNAMEs to the Cloudflare Tunnel since PR #156 and work from the LAN, so they should resolve publicly there too. Replace the address=/${BASE_DOMAIN}/ wildcard with explicit entries: the LAN-only names (derived from every controller:none HTTPRoute), gitea/nextcloud (off Cloudflare's 100 MB / 100 s limits), keycloak/auth, and the L4 VIPs. Add L0 check pihole-lan-names, mutation-tested. Correct docs that still described public names as CNAME to wan. and the cloudflared entrypoint comment. Co-Authored-By: Claude Opus 5.5 <[email protected]>
6.6 KiB
date, branch, title
| date | branch | title |
|---|---|---|
| 2026-11-09 | feat/pihole-explicit-lan-names | PiHole answers an explicit LAN name list instead of a zone wildcard (public names resolve to Cloudflare from the LAN) |
What changed
catalog/pihole.yaml → apps/wave-0/pihole.yaml. FTLCONF_misc_dnsmasq_lines no longer carries
address=/${BASE_DOMAIN}/${GATEWAY_VIP}. Every name not listed is now forwarded to 8.8.8.8/8.8.4.4 and
gets the public answer. The final list (local-ttl=60 kept):
Name (.${BASE_DOMAIN}) |
Answer | Why |
|---|---|---|
| jackett, pihole, plex, qbittorrent, radarr, semaphore, sonarr, headlamp | ${GATEWAY_VIP} |
LAN-only: every HTTPRoute with external-dns.alpha.kubernetes.io/controller: none, derived from the tree. No public record, tunnel 404s them; this entry is their only path |
| gitea, nextcloud | ${GATEWAY_VIP} |
public, but Cloudflare's proxy caps bodies at 100 MB / responses at 100 s (git push, LFS, uploads) |
| keycloak, auth | ${GATEWAY_VIP} |
keep the OIDC flow of the LAN-path apps on one path |
| unifi | ${UNIFI_L4_VIP} |
L4 VIP, not behind the Gateway |
| crafty-mc | ${CRAFTY_L4_VIP} |
L4 VIP, not behind the Gateway |
| pihole-dns | ${PIHOLE_DNS_VIP} |
the resolver's own address |
| plex-vip | ${PLEX_L4_VIP} |
kept, see below |
Not listed, so they resolve to Cloudflare from the LAN: the apex, grafana, crafty, seerr.
The derived LAN-only set matches the brief's list exactly (8 names; the tree was walked, not the
list trusted). The runbooks/serving route has no hostnames: but is not disowned (it inherits the
apex, public).
plex-vip is kept. Nothing advertises it any more (ADVERTISE_IP is a plex.direct name since
2026-10-05), but it is not unused: it is Plex's customCertificateDomain (CERTDOMAIN, Secret
plex-vip-tls), the name of the tcp/32400 VIP, and the 404'd name in the tunnel config. It has no
public record, so this entry is its only resolution.
New L0 check conformance/tests/l0-static/pihole-lan-names (scope: whole-repo, so picked up by
the runner's scope selection with no other wiring). Asserts: (a) no zone-wide entry (zone, #, or any
parent of the zone); (b) every controller: none hostname → ${GATEWAY_VIP}, and a disowned route
with no hostnames: fails; (c) every tunnel-404 hostname has an entry, with the L4 names (derived
from Services carrying both the external-dns hostname and an lbipam VIP) on their own VIP; (d) gitea
and nextcloud → ${GATEWAY_VIP}; plus catalog == rendered value and a built-in mutation self-test.
Stale docs corrected: platform/pm-agent/README.md, runbooks/pm-agent.md,
platform/externaldns/README.md, platform/cloudflared/README.md, platform/gateway/gateway.yaml
(comment), platform/headlamp/httproute.yaml (comment), apps/vms/unifi/README.md, README.md,
schemas/renderer-contract.md; runbooks/cloudflare-tunnel.md gains the two consequences below.
platform/cloudflared/deployment.yaml: the image entrypoint comment was wrong (comment-only).
What I verified
- Render: the baseline
render.py --no-image-automation catalog/pihole.yamlwas byte-identical to the committedapps/wave-0/pihole.yamlbefore the change, and the file was replaced by that command's output after it;--emit-waveswas not run. - Mutation, against the real files (10 mutants, each rc 1 with the right assertion, each restored):
(a) zone wildcard restored; (b) headlamp and sonarr entries removed; (b) a disowned route with no
hostnames:; (c) a new tunnel-404 host with no entry; (c) pihole-dns removed; (c) unifi pointed at the Gateway; (d) gitea removed; (d) nextcloud repointed; (0) rendered value drifted from the catalog. - Rollout mechanics, measured live (read-only): StatefulSet
updateStrategyRollingUpdate partition 0,podManagementPolicy: Parallel, nominReadySeconds, 2 replicas, PDB minAvailable 1. The last rollout (2026-11-08 06:47–06:48Z) replaced pihole-1 then pihole-0: FTL logged "started" ~1 s after the container, the pod went Ready ~16 s after the container, and pihole-0 started ~9 s later. - Expected LAN DNS blip: no total outage; one replica answers throughout. Each of the two pod
replacements can lose a few UDP queries for ~1–2 s while the terminating pod leaves the Service
(clients retry). For roughly 30–60 s the two pods answer differently for the apex, grafana, crafty
and seerr; both answers work. Cached answers age out in 60 s (
local-ttl). - Live DNS before the change: @8.8.8.8 returns Cloudflare (104.21.51.91 / 172.67.178.27) for the apex, grafana, seerr, crafty and gitea, and nothing for jackett; @192.168.2.23 returns 192.168.2.20 for all of them (the wildcard).
- Cloudflared image:
skopeo inspect --configon the pinned digest (amd64): Entrypoint["cloudflared"], Cmd["version"], User65532:65532.
What I did NOT verify
- The post-merge behaviour. Nothing here ran against a live PiHole with the new list; the
expected answers are from dnsmasq semantics and the tree. After reconcile, check
dig @192.168.2.23for one name per row, and grafana returning Cloudflare. - Whether every LAN client path works via Cloudflare. Measured 2xx/3xx through Cloudflare by the brief, not re-measured from a LAN client here.
- The FTL DNS-listening moment. "FTL started" is a log line, not a successful query; the rollout timing above assumes DNS is up shortly after it.
Accepted consequences, stated plainly:
- The apex, grafana, crafty and seerr now depend on the internet and the tunnel from the LAN too. An ISP or Cloudflare outage, or stopping the connectors, makes them unreachable from the LAN where they previously kept working. If grafana in particular should survive an internet outage, add it to the list (one entry; the check permits it).
- The tunnel rollback (revert to
wan.) now also requires restoring the wildcard; recorded inrunbooks/cloudflare-tunnel.md.
Flagged, not changed:
- The
piholecontainer has no readiness probe; only theoauth2-proxysidecar does, so "Ready" says nothing about DNS. The measured margin (FTL ~1 s, Ready ~16 s) makes this benign in a normal rollout but a slow FTL start would not hold the rollout back. A DNS readiness probe needs a schema/renderer decision.
Stale statements left alone: platform/coredns/coredns-custom.yaml header ("both resolve the whole
zone ... by wildcard", brief said not to touch CoreDNS); clusters/base/cluster-vars.yaml comments
(sensitive path); schemas/renderer-contract.md §exposure first bullets (manual edge port-forward
wording); historical infra/PROD-PROVISIONING.md, platform/externaldns/README.md TODO list.