Cloudflare Tunnels to Securely Expose Kubernetes Services
If you’re running a homelab Kubernetes cluster — like my Raspberry Pi 5 cluster — you’ve probably hit the same wall: you want to expose a service to the internet, but you don’t want to poke holes in your firewall or deal with dynamic IP headaches. Cloudflare Tunnels solve this elegantly. Here’s how I wired it all up with cloudflared, HashiCorp Vault, ExternalSecrets, and FluxCD.
How It Works
Cloudflare Tunnels work by running a lightweight daemon (cloudflared) inside your cluster. This daemon opens an outbound connection to Cloudflare’s edge — so no inbound ports need to be opened. Traffic hits yourdomain.com, Cloudflare routes it through the tunnel, and cloudflared forwards it to your internal service.
No public IP. No open ports. No reverse proxy configuration. Just a credential file and a config.
Step 1: Create the Tunnel
First, authenticate cloudflared and create a named tunnel:
This generates a credentials JSON file (e.g. 1234-5678-abcd.json) that looks like:
This file is the key to your tunnel — treat it like a password.
Step 2: Store the Credentials in Vault
Rather than committing the credentials file to Git, I store it in HashiCorp Vault and sync it into Kubernetes via ExternalSecrets:
Step 3: Sync the Secret with ExternalSecrets
An ExternalSecret resource pulls the credential from Vault and materializes it as a native Kubernetes secret:
Once applied, ExternalSecrets creates a tunnel-credentials Kubernetes secret containing credentials.json. You can verify it:
Step 4: Deploy cloudflared
The deployment mounts both the credentials secret and a ConfigMap that holds the tunnel configuration:
A few things worth noting:
- Two replicas give you redundancy — if one pod dies, the tunnel stays up.
- The liveness probe hits
/ready, whichcloudflaredexposes only when it has an active connection to Cloudflare’s edge. This means Kubernetes will restart unhealthy tunnel pods automatically. - The ingress rules are evaluated top-to-bottom, with a catch-all
404at the end — same pattern as a reverse proxy config.
Step 5: Configure DNS
In the Cloudflare dashboard (or via CLI), point your hostname at the tunnel:
This creates a CNAME record pointing uclab.dev to your tunnel’s .cfargotunnel.com address. No A record, no IP — fully managed by Cloudflare.
The Full Flow
Everything is GitOps-friendly: the only thing outside of Git is the raw secret value in Vault. No credentials are ever committed, and rotating the secret is as simple as a vault kv put followed by ExternalSecrets picking it up within 15 seconds.