Files
home-network-k3s-impl/platform/gateway/gateway.yaml
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

103 lines
5.0 KiB
YAML

---
# Shared Gateway — HTTPS listeners on *.home.keithjackson.dev, TLS terminated with the wildcard cert
# (docs/09). Implemented by Cilium (GatewayClass `cilium`). The ingress VIP is announced via Cilium
# L2 announcement on the flat LAN (docs/09, OQ-1 resolved). Phase 0 STUB.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared
namespace: gateway
annotations:
# PUBLIC DNS TARGET = THE CLOUDFLARE TUNNEL (2026-11-08, operator decision; replaces the
# 2026-10-03 `wan.${BASE_DOMAIN}` target). external-dns v0.23.0's gateway-httproute source reads
# `target` from the GATEWAY only (source/gateway.go: `overrides: annotations.
# TargetsFromTargetAnnotation(gw.Annotations)`) — never from an HTTPRoute — so this one value moves
# EVERY published route on this Gateway at once. Each published `<svc>.${BASE_DOMAIN}` (and the
# apex) becomes a CNAME to `<tunnel-uuid>.cfargotunnel.com`, and platform/externaldns/helmrelease.yaml
# sets `--cloudflare-proxied` so the CNAME is orange-clouded: public clients resolve Cloudflare
# anycast, Cloudflare terminates edge TLS (Total TLS) and hands the request to the in-cluster
# cloudflared connectors (platform/cloudflared), which forward to this Gateway's ClusterIP. No
# origin address of ours appears in public DNS, and no inbound WAN port is needed for HTTP(S).
#
# THE UUID IS A LITERAL, AND IT MUST EQUAL `tunnel:` IN platform/cloudflared/config.yaml. A CNAME
# to any other <uuid>.cfargotunnel.com is a dead name (Cloudflare answers 1016). That equality is
# asserted on every gate run by conformance/tests/l0-static/internal-services-publish-no-public-dns
# (assertion 9a). A literal rather than a cluster-var keeps this change off clusters/**.
#
# Scope: every route on this Gateway that publishes at all. `internal` services (and the
# operator-designated LAN/VPN-only names: plex, headlamp) stay unpublished via their
# `external-dns.alpha.kubernetes.io/controller: none` route annotation, which external-dns
# applies BEFORE it reads this target. Those same names map to `http_status:404` in the tunnel's
# ingress rules, so even a hand-made record cannot reach them through the tunnel.
#
# LAN and in-cluster clients are unaffected: PiHole and CoreDNS answer ${BASE_DOMAIN} with
# ${GATEWAY_VIP} directly (split-horizon), so they never take this public path.
#
# `wan.${BASE_DOMAIN}` (platform/externaldns/ddns.yaml) is NO LONGER a target of anything here;
# it stays only as the WireGuard udp/51820 endpoint.
external-dns.alpha.kubernetes.io/target: "fc81938b-5855-47f1-8a27-6286f56dda93.cfargotunnel.com"
spec:
gatewayClassName: cilium
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.${BASE_DOMAIN}"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-home-tls # produced by wildcard-certificate.yaml
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
# App namespaces opt in to attach HTTPRoutes here (WS-APPS renders the label +
# ReferenceGrant from ServiceDefinition `exposure`). docs/09.
gateway-access: "shared"
- name: http-redirect
protocol: HTTP
port: 80
hostname: "*.${BASE_DOMAIN}"
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "shared"
# APEX listeners. The wildcard listeners above match ONLY subdomains: `*.home.keithjackson.dev`
# does NOT intersect the bare apex `home.keithjackson.dev`, so an HTTPRoute for the apex host
# (the Homepage landing dashboard, clusters/base/homepage.yaml) would bind to NO listener and
# never serve. These two listeners give the apex its own hostname. TLS is the SAME wildcard cert:
# `home.keithjackson.dev` is already a SAN on wildcard-certificate.yaml, so `wildcard-home-tls`
# terminates the apex too — no extra Certificate needed. Same gateway-access=shared opt-in.
- name: https-apex
protocol: HTTPS
port: 443
hostname: "${BASE_DOMAIN}"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-home-tls # apex is a SAN on the wildcard cert (wildcard-certificate.yaml)
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "shared"
- name: http-apex-redirect
protocol: HTTP
port: 80
hostname: "${BASE_DOMAIN}"
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "shared"
# TODO(WS-NET, Phase 2): pin the L2-announced VIP / IP pool; add HTTP->HTTPS redirect HTTPRoute;
# confirm cross-namespace ReferenceGrant pattern with WS-APPS. Ingress opens NORTH-SOUTH only —
# it does not relax east-west default-deny (docs/09).