Gateway-published records now CNAME to wan.${BASE_DOMAIN}, which a
favonia/cloudflare-ddns Deployment keeps at the home WAN's current public
IP. Operator decision: every Gateway-published name goes public; internal
services (controller: none) remain unpublished.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.2 KiB
date, branch, title
| date | branch | title |
|---|---|---|
| 2026-10-03 | feat/public-dns-wan-target | Public DNS now points at the WAN — Gateway-published names CNAME to an in-cluster DDNS record |
What changed
The operator closed the open "public-target gap" recorded in
platform/externaldns/helmrelease.yaml. The WAN NAT / port-forwarding (80/443 → Gateway VIP
192.168.2.20) was done by the operator outside this repo.
Operator decisions (2026-10-03):
- Scope: every Gateway-published name goes public. That covers the five
exposure: publicapps (plex, gitea, keycloak, nextcloud, seerr) and also the hand-authored admin UIs (auth, crafty, grafana, headlamp, thehome.keithjackson.devapex).internalservices still publish nothing: theircontroller: noneroute annotation is checked before the target is read. - The WAN IP is kept current in-cluster, not hardcoded.
Implementation:
platform/gateway/gateway.yaml:external-dns.alpha.kubernetes.io/target: wan.${BASE_DOMAIN}on the shared Gateway. Every gateway-sourced record changes fromA 192.168.2.20toCNAME wan.home.keithjackson.dev.platform/externaldns/ddns.yaml(new):favonia/cloudflare-ddns:1.17.1Deployment in theexternal-dnsnamespace. It reusescloudflare-api-tokenand keepswan.${BASE_DOMAIN}an unproxied IPv4 A record at the WAN IP (detected via Cloudflare trace, checked every 5 min,DELETE_ON_STOP=false). external-dns has no TXT registry entry for it, so it never owns or deletes it.- The helmrelease comment block and the README now describe the decision instead of the gap.
Known consequence: k3s nodes resolve through public DNS (node_baseline_dns_servers = 8.8.8.8 /
1.1.1.1). The kube-apiserver's OIDC issuer keycloak.home.keithjackson.dev therefore resolves to the
WAN IP from the nodes, and kubectl SSO works only if the router hairpins WAN:443 back to the Gateway.
A LAN-side probe of 136.51.115.252:443 on 2026-10-03 returned a self-signed placeholder cert,
not the Gateway's Let's Encrypt wildcard. That suggests no hairpin. The cert-based admin kubeconfig
is unaffected.
What I verified
- Live prod, 2026-10-03, before the change: Gateway
gateway/sharedADDRESS192.168.2.20. External-dns args include--source=gateway-httproute --source=service --policy=sync --txt-owner-id=prod --domain-filter=keithjackson.dev, and it logs "All records are already up to date". - Public answers at 1.1.1.1: plex/gitea/keycloak/grafana/auth/apex →
192.168.2.20;crafty-mc→.22andunifi→.21(service source, not affected by this change); jackett → none (internal, correctly unpublished). - The authority (betty.ns.cloudflare.com) has no unprefixed TXT, AAAA or MX at
plex.or the apex, so the A→CNAME switch has no co-existing record to collide with. - WAN IP from the workstation:
136.51.115.252. - No CiliumNetworkPolicy or NetworkPolicy in
external-dns. The Kyverno label and default-deny policies match onlyapp-namespacenamespaces. The vpn egress policy selects onlyegress-class: vpnpods. - Toolbox gate result: see the PR description (run after this commit).
What I did NOT verify
- The outside-in path. I have no vantage point outside the LAN. Whether
https://<svc>.home.keithjackson.devfrom the internet reaches the Gateway with the LE cert is UNVERIFIED. - Hairpin / kubectl SSO after the flip. Likely broken per the probe above. Fix candidates: a
node
/etc/hostspin forkeycloak.${BASE_DOMAIN}→ Gateway VIP vianode_baseline, or router NAT reflection. - The record-type flip itself, until Flux reconciles: whether Cloudflare accepts external-dns's create-CNAME in the same batch as the delete-A, or converges one interval later.
favonia/cloudflare-ddnsunder this pod securityContext. The README warns thatno-new-privilegesmisbehaves on some Proxmox-style hosts. These are bare-metal nodes, but it is untested until the pod runs.- Whether the L4 service-source names (crafty-mc, unifi) should also go public. Not decided, not changed.
conformance/tests/l0-static/internal-services-publish-no-public-dns/check.shheader lines 39–40 still describe the target as open. That is a sensitive path, so it is left for a follow-up.