Files
home-network-k3s-impl/decisions/2026-11-08-dns-via-cloudflare-tunnel.md
T
jakman85andClaude Opus 5.5 92944486d1
gate / gate (pull_request) Successful in 45s
gate / conformance (pull_request) Skipped
feat(dns): publish public names as proxied CNAMEs to the Cloudflare Tunnel
Gateway target -> fc81938b-...cfargotunnel.com, --cloudflare-proxied globally,
L4 service publishers (unifi, crafty-mc) pinned DNS-only, Plex web UI and
Headlamp disowned (LAN/VPN-only). L0 internal-services-publish-no-public-dns
gains assertion (9) tying the Gateway target to the tunnel UUID, requiring
cloudflare-proxied=false on every service-source publisher, and keeping the
tunnel's 404 list and the unpublished set consistent. L2 dns-exposure fixture
opts out of proxying.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 17:55:41 -06:00

5.5 KiB

date, branch, title
date branch title
2026-11-08 feat/dns-via-cloudflare-tunnel Public DNS via the Cloudflare Tunnel (Phase 2) — proxied CNAMEs, Plex/Headlamp LAN-only, L0 guard

What changed

One atomic change, so the conformance guard lands with the DNS flip.

  • platform/gateway/gateway.yaml: the shared Gateway's external-dns.alpha.kubernetes.io/target is now the literal fc81938b-5855-47f1-8a27-6286f56dda93.cfargotunnel.com (was wan.${BASE_DOMAIN}). Comment block rewritten.
  • platform/externaldns/helmrelease.yaml: extraArgs: [--cloudflare-proxied] (global, because v0.23.0 reads per-object cloudflare-proxied from the route/Service, never the Gateway). The public-target comment block rewritten, including the prefix-regression risk.
  • apps/vms/unifi/50-service-l4.yaml, platform/crafty/service-l4.yaml: external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" (L4 A records stay DNS-only). They are the only service-source publishers in the tree and live (measured).
  • Plex web UI LAN/VPN-only: catalog/plex.yaml exposure: public → internal; apps/wave-0/plex.yaml re-rendered with the canonical command into a scratch dir and ONLY plex.yaml copied back. The render diff is exactly +2 lines (annotations: external-dns.alpha.kubernetes.io/controller: none on HTTPRoute plex-32400). No other golden moved, and nothing was onboarded (vm-demo/keycloak files were not touched). ADVERTISE_IP, the L4 VIP .24 and plex-remote.yaml are untouched.
  • Headlamp LAN/VPN-only: controller: none on the hand-authored platform/headlamp/httproute.yaml.
  • conformance/tests/l0-static/internal-services-publish-no-public-dns/check.sh: header and register prose updated; new assertion (9): (9a) Gateway target == <tunnel: from platform/cloudflared/config.yaml>.cfargotunnel.com, and --cloudflare-proxied is set; (9b) every service-source publisher has cloudflare-proxied: "false" and no HTTPRoute opts out of proxying; (9c) no undisowned route names a hostname the tunnel 404s, the only service publishers of a 404'd name are the declared L4 set {unifi, crafty-mc}, and every disowned route's hostname IS 404'd by the tunnel (lists kept consistent both ways); (9d) plex and headlamp pinned disowned. Its chainsaw-test.yaml gap prose updated.
  • conformance/tests/l2-behavioral/dns-exposure/chainsaw-test.yaml: fixture Service gets cloudflare-proxied: "false", else the global flag proxies it and the exact-IP poll fails.
  • Docs: platform/externaldns/ddns.yaml header (wan. is WireGuard-only), platform/externaldns/README.md, platform/gateway/README.md, runbooks/plex-remote-access.md, runbooks/cloudflare-tunnel.md (Phase 2 done; operator step to close WAN tcp/80+443 and keep udp/51820 + tcp/32400; rotation now also swaps the Gateway UUID; rollback path).

What I verified

  • external-dns v0.23.0 source, read at the tag: source/gateway.go takes target overrides from gw.Annotations and provider-specific (incl. cloudflare-proxied) from the ROUTE's annotations; annotations.SetAnnotationPrefix re-derives CloudflarePrefix, so the alpha-prefixed key is the one read here; provider/cloudflare shouldBeProxied starts from --cloudflare-proxied and a parsed "false" annotation overrides it.
  • (8b) only forbids annotation-prefix / enable-legacy-annotation-prefix in extraArgs; a mutation confirmed --cloudflare-proxied passes and --annotation-prefix=x/ fails.
  • The L0 check passes (116 assertions) and each new assertion FAILS on its mutation: Gateway target back to wan (9a), flag removed (9a), unifi proxied "true" (9b), headlamp undisowned (9c,9d), plex golden undisowned (9c,9d,(1)), jackett removed from the tunnel's 404 list (9c). cloudflared-ingress-rules still passes; render.py --selftest PASS.
  • Live, read-only (kubectl k3s-prod, ~23:55Z): 17 HTTPRoutes; the six internal apps carry controller: none; plex and headlamp do not (yet); the only Services with an external-dns hostname are crafty/crafty-l4-tcp and unifi/unifi-l4-tcp; Gateway target is currently wan.home.keithjackson.dev.
  • Toolbox gate result: see the PR description (rc and skipped stages quoted there).

What I did NOT verify

  • Anything after merge: that external-dns actually UPDATEs the eight public names to proxied CNAMEs to the tunnel, DELETEs plex + headlamp and their TXT, and leaves unifi/crafty-mc/wan untouched. Expected effect is listed in the PR; the PM must read external-dns logs and dig @1.1.1.1.
  • A rendered HelmRelease: I read the chart 1.23.0 templates/deployment.yaml (a list extraArgs is emitted item-by-item via tpl . $ | quote), but did not helm template it.
  • kubectl OIDC from nodes: nodes resolve publicly, so keycloak. discovery/JWKS now goes through Cloudflare + the tunnel. No oidc-ca-file is set (system roots), so the edge cert should validate, but Cloudflare bot/WAF settings on that path are untested.
  • RISK: if the annotation prefix stops being honoured at runtime again (11-07 incident class), target is ignored and every name becomes a PROXIED A record at 192.168.2.20, while internal names publish. Assertion (8) only keeps the tree and values.annotationPrefix consistent; it cannot catch a controller that ignores the flag. No runtime alert exists for it.
  • The router WAN 80/443 close is an operator step, not done. The Pi-hole wildcard trim is a later PR.
  • Follow-up: spec requirement IDs for this (proposed R-DNS-04/05, R-SEC-04) once the spec ADR merges; platform/pm-agent/README.md and a comment in catalog/pihole.yaml still describe the wan. CNAME.