# platform/cloudflared **Cloudflare Tunnel connector**: inbound HTTPS from the Cloudflare edge to the shared Gateway, outbound-only from the cluster (no router port forward, no hairpin, no split-DNS dependency). - **Flux tier:** `clusters/base/cloudflared.yaml` — its own Kustomization, `dependsOn: [platform-config]`, `wait: false`, and nothing depends on it. A missing or invalid credential can affect only the tunnel. - **Runbook (enrolment, canary, alerts, rollback):** `runbooks/cloudflare-tunnel.md` - **Design record:** `decisions/2026-11-08-cloudflare-tunnel.md` - **Guard:** `conformance/tests/l0-static/cloudflared-ingress-rules` | File | What it is | |---|---| | `config.yaml` | The locally-managed tunnel: literal tunnel UUID + ingress rules. LAN/VPN-only hosts → 404, apex + wildcard → `https://cilium-gateway-shared.gateway.svc.cluster.local:443` with `matchSNItoHost`, catch-all 404. Never the Kubernetes API. | | `cloudflared-credentials.sops.yaml` | SOPS-encrypted tunnel credentials JSON. Ships with `REPLACE-ME`; the operator fills it. | | `deployment.yaml` | 2 replicas, hard spread across nodes, non-root, read-only root fs, pinned digest; PDB `minAvailable: 1`. | | `networkpolicy.yaml` | default-deny (single owner — not an app namespace) + Prometheus scrape allow + egress CNP: world 7844 tcp/udp and 443, the Gateway (`ingress` entity), and Gateway backends by `gateway-access=shared` minus the LAN-only namespaces. | | `servicemonitor.yaml`, `alerts.yaml` | Headless metrics Service + ServiceMonitor scraping :2000, and the two alerts, both written to fire (not go quiet) when their series vanish. | **Phase 1 only.** This component changes no live traffic: Cloudflare sends a hostname through the tunnel only when its DNS record is a CNAME to `.cfargotunnel.com`, and nothing here creates one. Moving DNS (external-dns, Gateway annotations, Pi-hole/CoreDNS split horizon) is a later change.