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]>
77 lines
4.3 KiB
Markdown
77 lines
4.3 KiB
Markdown
# platform/externaldns
|
|
|
|
**ExternalDNS** — automatic public DNS records for `public` services (Cloudflare).
|
|
|
|
- **Spec:** [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md)
|
|
(ExternalDNS is named in [docs/01](../../../home-network-k3s/docs/01-architecture-overview.md); no
|
|
dedicated ADR)
|
|
- **Serves:** R-DNS-01/02/03
|
|
- **Owner:** WS-NET (token from WS-SEC)
|
|
|
|
## 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.
|
|
- **Pods** resolve the whole zone to the Gateway VIP via CoreDNS (split-horizon) and never use the
|
|
public path. **LAN clients** use PiHole, which since 2026-11-09 answers an EXPLICIT list
|
|
(`catalog/pihole.yaml`): the LAN-only names, plus gitea/nextcloud/keycloak/auth (kept off
|
|
Cloudflare's 100 MB / 100 s limits and on one OIDC path), go to the Gateway VIP and the L4 names to
|
|
their own VIPs; every other public name (apex, grafana, crafty, seerr) resolves to Cloudflare from
|
|
the LAN exactly as from anywhere else, so no split-DNS is needed for them. Guard:
|
|
`conformance/tests/l0-static/pihole-lan-names`.
|
|
- **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.
|