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]>
This commit is contained in:
jakman85andClaude Opus 5.5 committed 2026-11-08 12:38:22 -06:00
1 parent eb62c13c6b
commit aabc8c60b6
22 files changed
+1579

No files matched your search

@@ -0,0 +1,34 @@
# =============================================================================
# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize and never
# auto-encrypted by SOPS). Reference scaffold + OPERATOR HAND-OFF.
#
# WHAT THIS IS: the credentials JSON of the locally-managed Cloudflare Tunnel that
# platform/cloudflared runs. `cloudflared tunnel create` writes it; it holds the account tag, the
# tunnel UUID and the tunnel SECRET. Anyone holding it can run a connector for this tunnel and
# receive its traffic, so it is a real credential: SOPS-encrypted in git, never printed.
#
# *** THE REAL FILE IS COMMITTED WITH REPLACE-ME PLACEHOLDERS, ALREADY ENCRYPTED. ***
# So the wiring is complete before the tunnel exists: the resource is listed in kustomization.yaml
# and both overlays already carry the `cloudflared` decryption patch. Until you fill it, the
# Deployment CrashLoops inside its own NON-GATING Flux Kustomization (clusters/base/cloudflared.yaml,
# wait:false) and CloudflaredTunnelDown fires. Nothing else in the cluster is affected.
#
# HAND-OFF (operator — an agent must not mint a long-lived credential, CLAUDE.md §6).
# The full procedure, with verification and rollback, is runbooks/cloudflare-tunnel.md. In short:
# 1. `cloudflared tunnel login` + `cloudflared tunnel create --credentials-file ... home-k3s`
# (pinned cloudflared image under podman; the toolbox does not ship cloudflared).
# 2. Write the JSON STRAIGHT INTO the encrypted file with `sops set`, inside the toolbox — the
# plaintext never reaches a terminal or the repo tree.
# 3. Put the tunnel UUID (not secret) into platform/cloudflared/config.yaml `tunnel:`.
# 4. Verify without printing (`sops -d --extract ... | jq -e ...`), then PR it.
# =============================================================================
apiVersion: v1
kind: Secret
metadata:
name: cloudflared-credentials
namespace: cloudflared
type: Opaque
stringData:
# One line of JSON, exactly as `cloudflared tunnel create` wrote it (compacted with `jq -c`).
# TunnelID MUST equal the `tunnel:` UUID in config.yaml.
credentials.json: '{"AccountTag":"REPLACE-ME","TunnelSecret":"REPLACE-ME","TunnelID":"REPLACE-ME"}'