Files
home-network-k3s/docs/09-dns-and-ingress.md
T
jakman85andClaude Opus 5.5 28d0e0a0f2 ADR-0016 (Proposed): public ingress via Cloudflare Tunnel
Record the operator's 2026-11-08 decision to move public HTTP(S) ingress
from edge port-forwarding of 80/443 to an outbound-only Cloudflare Tunnel
(in-cluster cloudflared, proxied CNAMEs via ExternalDNS). Motivation: the
GFiber router cannot hairpin NAT and advertises Google IPv6 DNS via RA, so
LAN clients bypass Pi-hole split horizon and fail.

Docs 00/01/03/07/08/09/13/14, GLOSSARY and definition-of-done brought into
line. R-DNS-01..03 and R-TLS-01..03 wording unchanged. New requirements:
R-DNS-04 (no 80/443 forward, resolver-independent LAN reachability),
R-DNS-05 (only `public` services at the tunnel; 404 catch-all; no public
record for `internal`), R-SEC-04 (Kubernetes API never publicly reachable).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:38:16 -06:00

8.4 KiB

09 — DNS & Ingress

Serves: R-DNS-01/02/03/04/05, R-C2-01, R-SEC-04, works with R-TLS-*. Related: ADR-0002 (Cilium/Gateway API), ADR-0016 (Cloudflare Tunnel public ingress).

Naming model

  • All services live at <service>.home.keithjackson.dev (e.g. plex.home.keithjackson.dev).
  • The root home.keithjackson.dev serves the Command & Control UI (12).
  • *.home.keithjackson.dev is the platform's zone, in Cloudflare DNS. public services are published as proxied CNAMEs to the Cloudflare Tunnel (<tunnel-uuid>.cfargotunnel.com, ADR-0016), not as records pointing at the network's public IP.

DNS automation (R-DNS-02)

  • ExternalDNS watches Gateway/HTTPRoute (and Service) resources and manages records in the authoritative DNS provider (Cloudflare, per OQ-4) automatically — a new service's DNS appears from its declared intent, no manual record editing.
  • Records for *.home.keithjackson.dev are created/updated/removed as services come and go.
  • Public records are created only for exposure: public services (R-DNS-05). The DNS-only (unproxied) wan. record for WireGuard is kept current by cloudflare-ddns, outside this flow.

Split-horizon resolution (R-DNS-03)

Internal and external answers differ:

  • External (<service>.home.keithjackson.dev from the internet): a public service resolves to Cloudflare's edge (proxied CNAME to the tunnel). Cloudflare forwards the request down the outbound-only tunnel to cloudflared in the cluster, which forwards it to the Gateway (ADR-0016). internal services have no public answer.

  • Internal (LAN clients): Pi-hole answers with the internal Gateway address (or the service's L4 VIP) for the names it holds local records for.

  • Correctness does not depend on split horizon for public web apps (R-DNS-04). The ISP router cannot hairpin NAT and advertises a public IPv6 resolver via RA, so some LAN clients will get the public answer. That answer goes through Cloudflare and works from the LAN too.

  • PiHole (itself an onboarded service) provides internal LAN DNS. It keeps explicit local records for:

    • the internal names;
    • the L4 VIPs;
    • the names that must avoid Cloudflare's 100 MB body / 100 s timeout limits or must work during an internet outage: gitea, nextcloud, keycloak/auth.

    A trusted certificate is served either way: Cloudflare's edge certificate on the tunnel path, the Let's Encrypt wildcard on the LAN path (08).

  • In-cluster resolution keeps its own split horizon (CoreDNS) and the node /etc/hosts pins (OIDC issuer, Gitea registry), so cluster-internal callers never traverse Cloudflare.

DNS is thus both a platform capability and a hosted service. Bootstrapping order matters: the platform needs working DNS before PiHole is fully managed — handled by the bring-up sequence in roadmap/phases.md.

Ingress (north-south) via Gateway API

  • Gateway API implemented by Cilium is the ingress layer (no separate ingress controller; reuses the CNI, ADR-0002).
  • A shared Gateway defines HTTPS listeners on *.home.keithjackson.dev terminating TLS with the cert-manager cert (08).
  • Each service contributes an HTTPRoute mapping its hostname to its Service — rendered from the ServiceDefinition, not hand-authored.
  • Exposure class is declared per service:
    • internal — reachable only on the LAN/VPN (no tunnel route, no public record; internal DNS only).
    • public — additionally reachable from the internet through the Cloudflare Tunnel: cloudflared → the Gateway over HTTPS (ADR-0016).
  • Never public: jackett, pihole, semaphore, qbittorrent, radarr, sonarr, headlamp (a front end to the Kubernetes API), plex's web UI, and the non-HTTP L4 services unifi and crafty-mc.
  • The Kubernetes API is never published — no tunnel route, no edge forward, no public record (R-SEC-04).

Public ingress: Cloudflare Tunnel (ADR-0016)

  • cloudflared runs in-cluster as a Flux-managed workload and holds an outbound-only connection to Cloudflare. No inbound 80/443 reaches the WAN IP.
  • Its ingress rules list one route per public hostname → the Gateway (HTTPS, origin certificate validated with the hostname as SNI). The rules end in a catch-all http_status:404, so an unlisted hostname is refused at the tunnel (R-DNS-05).
  • Edge TLS is Cloudflare ACM/Total TLS (Universal SSL does not cover two-label-deep names); origin TLS is the Let's Encrypt wildcard at the Gateway (08).
  • Limits and trade-offs:
    • 100 MB request bodies and 100 s responses through the proxy;
    • Cloudflare terminates TLS and sees plaintext;
    • a Cloudflare outage takes public access down (the LAN keeps working through Pi-hole's local records);
    • the client IP arrives as CF-Connecting-IP / X-Forwarded-For, not as the TCP source.

Edge integration (single SOHO router — OQ-1 resolved)

  • The environment has one SOHO edge router: no VLANs, no VPN, no services, no API/IaC. It does not forward 80/443 (public web ingress is the tunnel, ADR-0016). Its only remaining inbound forwards are WireGuard udp/51820 (remote access, via the DNS-only wan. record) and Plex tcp/32400 (remote streaming via plex.tv; Cloudflare's terms restrict proxied video). This is a documented manual edge step (the SOHO device has no automation), the analogue of the old pfSense manual-config exception. The WAN IP therefore stays public.
  • No multi-WAN inbound rules — single WAN (03).
  • internal services are never port-forwarded or tunnelled; they rely on internal DNS + Gateway (or their L4 VIP) on the LAN, or WireGuard from outside.
  • Ingress VIP mechanism — resolved: Cilium L2 announcement (ARP/NDP) on the flat LAN. BGP is not available on a SOHO router, and a single flat subnet makes L2 announcement the natural choice.

DNS record depth (note for *.home.keithjackson.dev)

A wildcard *.keithjackson.dev matches only one label deep — it covers home.keithjackson.dev but not plex.home.keithjackson.dev. Consequences:

  • internal services need no public record — PiHole resolves <svc>.home.keithjackson.dev to the internal Gateway VIP (split-horizon). This is the majority of services.
  • public services need a public record for <svc>.home.keithjackson.dev: a proxied CNAME to the tunnel (ADR-0016). Per OQ-4 (resolved), ExternalDNS manages these automatically via the scoped Cloudflare API token — no manual record editing (see 08). The same depth limit is why edge TLS needs ACM/Total TLS rather than Universal SSL.

Interaction with default-deny

Ingress opens north-south paths to the specific service; it does not relax east-west default-deny (03). A service being publicly reachable does not let other in-cluster services reach it unless an explicit allow rule exists.

Acceptance gates

  • A new service's <service>.home.keithjackson.dev record appears automatically and routes to it.
  • internal services are unreachable from the public internet; public services are reachable and TLS-valid.
  • Internal clients resolving through Pi-hole reach the internal Gateway for the names it holds local records for, and get a trusted cert.
  • A LAN client resolving a public name through a public resolver reaches it, TLS-valid (R-DNS-04). The WAN IP does not answer on 80/443.
  • A request at the tunnel for any hostname that is not a public service returns 404, and no internal service has a public DNS record (R-DNS-05).
  • The Kubernetes API is unreachable from the internet: no tunnel route, no edge forward, no public record (R-SEC-04).
  • The root home.keithjackson.dev serves the C2 UI.

Resolved

  • OQ-1 — single SOHO edge; ingress VIP via Cilium L2 announcement. Public web ingress amended 2026-11-08 to a Cloudflare Tunnel (ADR-0016); the edge keeps only udp/51820 and tcp/32400.
  • OQ-4 — scoped Cloudflare API token drives ExternalDNS (records) and cert-manager DNS-01 (certs) automatically; see 08.