Files
home-network-k3s-impl/clusters/base/cloudflared.yaml
jakman85andClaude Opus 5.5 aabc8c60b6 feat(cloudflared): Cloudflare Tunnel connector, Phase 1 (isolated, no traffic change)
Adds platform/cloudflared: a locally-managed tunnel whose ingress rules live in
git. LAN/VPN-only hosts (plex, plex-vip, jackett, pihole, pihole-dns, semaphore,
qbittorrent, radarr, sonarr, headlamp, unifi, crafty-mc) map to 404. The apex
and *.home go to cilium-gateway-shared.gateway over TLS with matchSNItoHost.
Last comes a catch-all 404. Nothing routes to the Kubernetes API.

- 2-replica Deployment, hard node spread, non-root, pinned digest, PDB
- default-deny plus an egress CNP: Cloudflare edge 7844/443, the Gateway, and
  Gateway backends by gateway-access=shared minus the LAN-only namespaces
  (needed because of the CVE-2025-30162 L7LB egress re-check)
- PodMonitor plus two alerts that fire on absent series, with promtool tests
- its own Flux Kustomization (dependsOn platform-config, wait:false, nothing
  depends on it), plus SOPS decryption patches in both overlays
- placeholder credentials (SOPS-encrypted) so it can merge before enrolment
- L0 cloudflared-ingress-rules, and a cloudflared_ scrape owner
- runbooks/cloudflare-tunnel.md and decisions/2026-11-08-cloudflare-tunnel.md

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

62 lines
3.5 KiB
YAML

---
# Flux Kustomization: Cloudflare Tunnel connector — reconciles ./platform/cloudflared.
#
# ISOLATION IS THE POINT OF THIS FILE. The Deployment it applies CANNOT become Ready until the
# operator enrols the tunnel (runbooks/cloudflare-tunnel.md): it ships with REPLACE-ME credentials
# and a placeholder UUID, and a wrong or revoked credential later produces the same state. So:
#
# * ITS OWN Kustomization, not a line in platform/kustomization.yaml. `platform` is wait:true and
# gates platform-config, which gates every apps wave; an unenrolled tunnel there would be a
# platform-wide stall ON MERGE, with the cause (a missing manual step) invisible from any Flux
# condition. Same reasoning, and same failure shape, as vpn-ingress's unfilled server key.
# * wait: false. Its pods are legitimately not-Ready whenever the credentials are absent or the
# edge is unreachable. Health is reported by the CloudflaredTunnelDown / ReplicasDegraded
# alerts (platform/cloudflared/alerts.yaml), not by this Kustomization's Ready condition —
# which reaches Ready on APPLY success.
# * NOTHING dependsOn THIS, and nothing should. A missing or invalid credentials Secret can then
# only ever affect the tunnel itself. conformance/tests/l0-static/cloudflared-ingress-rules
# fails the gate if any Kustomization gains `dependsOn: cloudflared`, or if this one stops
# being wait:false.
# * No Certificate, no HelmRelease, no CRD: nothing in the path for Flux's health checks to wait
# on even if wait were flipped (the Certificate-stall lesson).
#
# dependsOn: [platform-config] — A REAL EDGE.
# 1. CRD ORDERING: PodMonitor and PrometheusRule are monitoring.coreos.com CRs whose CRDs are
# installed by kube-prometheus-stack in `platform`; platform-config dependsOn platform, so this
# edge carries that guarantee transitively (Flux aborts a whole pass on an unknown kind).
# 2. THE FAR SIDE OF THE TUNNEL: the Gateway (and so the cilium-gateway-shared Service the ingress
# rules point at) is applied by platform-config. A tunnel that comes up before its origin is a
# support call.
#
# ZERO EFFECT ON LIVE TRAFFIC UNTIL DNS MOVES. Cloudflare only sends a hostname through a tunnel
# when that hostname's DNS record is a CNAME to <uuid>.cfargotunnel.com. This change creates no
# such record and leaves external-dns, the Gateway annotations, Pi-hole and CoreDNS untouched.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cloudflared
namespace: flux-system
spec:
interval: 10m
retryInterval: 2m
timeout: 5m
prune: true
wait: false
dependsOn:
- name: platform-config
sourceRef:
kind: GitRepository
name: flux-system
path: ./platform/cloudflared
# REQUIRED, not boilerplate: platform/cloudflared/config.yaml renders `${BASE_DOMAIN}` into every
# ingress hostname, and alerts.yaml into both runbook_url annotations. Without this block the
# token becomes the EMPTY STRING and the Kustomization still reports Ready (substitution fails
# OPEN), leaving ingress rules for `plex.` and `*.` — a tunnel that 404s everything at best.
postBuild:
substituteFrom:
- kind: ConfigMap
name: cluster-vars
# SOPS decryption is layered per-environment: clusters/dev and clusters/prod each patch THIS
# Kustomization by name with a `decryption:` stanza for cloudflared-credentials.sops.yaml.
# Without it the Secret applies cleanly full of ENC[...] and cloudflared cannot authenticate.