Files
home-network-k3s-impl/decisions/2026-11-09-pihole-explicit-lan-names.md
T
jakman85andClaude Opus 5.5 8fe8d5df8a
gate / gate (pull_request) Successful in 1m14s
gate / conformance (pull_request) Skipped
feat(dns): PiHole answers an explicit LAN name list, not a zone wildcard
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]>
2026-11-08 19:51:49 -06:00

6.6 KiB
Raw Blame History

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.yaml was byte-identical to the committed apps/wave-0/pihole.yaml before the change, and the file was replaced by that command's output after it; --emit-waves was 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 updateStrategy RollingUpdate partition 0, podManagementPolicy: Parallel, no minReadySeconds, 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 --config on the pinned digest (amd64): Entrypoint ["cloudflared"], Cmd ["version"], User 65532: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.23 for 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 in runbooks/cloudflare-tunnel.md.

Flagged, not changed:

  • The pihole container has no readiness probe; only the oauth2-proxy sidecar 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.