Files
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
..

platform/externaldns

ExternalDNS — automatic public DNS records for public services (Cloudflare).

What this stub is

Flux HelmRelease watching Gateway/HTTPRoute + Service resources, publishing <service>.home.keithjackson.dev records in Cloudflare from declared intent (no manual record editing, R-DNS-02).

Split-horizon note (docs/09)

A wildcard *.keithjackson.dev matches only one label deep, so <svc>.home.keithjackson.dev needs its own record. internal services need NO public record — PiHole resolves them to the internal Gateway VIP (split-horizon). ExternalDNS manages public services' records only.

Public target: the Cloudflare Tunnel, proxied (2026-11-08)

Gateway-published records are proxied CNAMEs to the Cloudflare Tunnel (platform/cloudflared), not the WAN IP and not the Gateway VIP:

gitea.home.keithjackson.dev  CNAME (proxied)  fc81938b-5855-47f1-8a27-6286f56dda93.cfargotunnel.com  <- external-dns
unifi.home.keithjackson.dev  A     (DNS-only) 192.168.2.21    <- external-dns, `service` source (L4)
crafty-mc.home...            A     (DNS-only) 192.168.2.22    <- external-dns, `service` source (L4)
wan.home.keithjackson.dev    A     (DNS-only) <current WAN IP> <- cloudflare-ddns (ddns.yaml): WireGuard udp/51820 ONLY
  • The target annotation is on the shared Gateway (platform/gateway/gateway.yaml); external-dns reads it from the Gateway only, so it applies to every route that publishes at all. Its UUID must equal tunnel: in platform/cloudflared/config.yaml (L0 check internal-services-publish-no-public-dns (9a)).
  • --cloudflare-proxied (HelmRelease extraArgs) orange-clouds every record by default — the only practical lever, because v0.23.0 reads the per-object cloudflare-proxied annotation from the route/Service, never the Gateway. Every service-source publisher must carry external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" (L4 ports cannot ride the HTTP proxy); (9b) fails the gate otherwise.
  • internal routes (controller: none) still publish nothing. Plex's web UI and Headlamp joined that set on 2026-11-08 (LAN/VPN-only); the tunnel also 404s every such name (9c/9d).
  • Public answers are Cloudflare anycast A and AAAA — Cloudflare's edge, not an origin address.
  • LAN and pods resolve *.home.keithjackson.dev to the Gateway VIP via PiHole and CoreDNS (split-horizon) and never use the public path.
  • Nodes resolve through public DNS (node_baseline_dns_servers), so from a node the apiserver's OIDC issuer keycloak.home.keithjackson.dev now resolves to Cloudflare and its discovery/JWKS fetches go through the tunnel (publicly trusted edge cert; no oidc-ca-file is configured). The router NAT hairpin is no longer on that path.

Check: dig +short @1.1.1.1 gitea.home.keithjackson.dev → Cloudflare anycast addresses (no CNAME is visible for a proxied record); kubectl -n external-dns logs deploy/external-dns.

History

2026-10-03 → 2026-11-08: records were DNS-only CNAMEs to wan.home.keithjackson.dev, with the router forwarding WAN tcp/80+443 to the Gateway VIP. Before 2026-10-03 they answered the Gateway VIP itself.

Secrets

Cloudflare API token referenced by name (cloudflare-api-token, shared with cert-manager), SOPS-encrypted (WS-SEC, OQ-4).

TODO (later phases)

  • Phase 2 (WS-NET): confirm gateway-httproute source; prove an auto record for a test route (R-DNS-02, Phase-2 exit gate).
  • Phase 6 (WS-APPS): PiHole provides the internal split-horizon answers as it onboards.