Cloudflare Tunnel (cloudflared) connects outbound from the cluster to Cloudflare’s edge. Public hostnames in the Zero Trust dashboard route to in-cluster Services over plain HTTP or HTTPS. No inbound ports on the home router are required.
Typical uses in this homelab:
| Hostname | Backend | Doc |
|---|---|---|
dex.<your-domain> | Dex OIDC broker | Dex OIDC |
argocd.<your-domain> | Argo CD UI | Argo CD |
immich.<your-domain> | Immich server | Immich |
Manifests live in the public homelabs repo under cloudflare/ (GitOps repositories).
Create the tunnel in Zero Trust#
- Open Cloudflare Zero Trust → Networks → Tunnels.
- Create a tunnel → choose Cloudflared.
- Name the tunnel (for example
homelab-k8s). - On the Install connector step, choose Docker (or any option) and copy the tunnel token — you need it for the Kubernetes Secret below.
- Finish the wizard; add Public Hostnames later (or now) for each service you want to expose.
Keep the token out of Git. Store it in a local secret.yaml (gitignored) or in your secret manager and inject at deploy time.
Deploy cloudflared in Kubernetes#
From a checkout of homelabs (or the equivalent path in your bootstrap flow):
kubectl apply -f cloudflare/namespace.yaml
cp cloudflare/secret.yaml.template cloudflare/secret.yaml
# Edit cloudflare/secret.yaml — set tunnel-token to the value from Zero Trust
kubectl apply -f cloudflare/secret.yaml
kubectl apply -f cloudflare/deployment.yamlThe Deployment runs a single cloudflared replica in namespace cloudflare-tunnel. It reads TUNNEL_TOKEN from Secret cloudflare-tunnel-token and executes tunnel run.
Verify:
kubectl -n cloudflare-tunnel get pods
kubectl -n cloudflare-tunnel logs -f deployment/cloudflaredHealthy logs show the connector registered with Cloudflare and tunnels ready.
Add a public hostname#
For each service, add a Public Hostname on the same tunnel in Zero Trust (Networks → Tunnels → your tunnel → Public Hostname).
| Field | Typical value |
|---|---|
| Subdomain | Service name (dex, argocd, immich, …) |
| Domain | Your public domain |
| Type | HTTP for plain in-cluster HTTP; HTTPS when the Service speaks TLS |
| URL | In-cluster DNS name and port (for example http://dex.dex.svc.cluster.local:5556) |
Service-specific notes:
- Dex — HTTP to port
5556. See Dex OIDC. - Argo CD — HTTPS to
argocd-server.argocd.svc.cluster.local:443with No TLS Verify (self-signed cert). See Argo CD. - Immich — HTTP to
immich-server.immich.svc.cluster.local:2283. See Immich.
Changes in the dashboard take effect without restarting cloudflared; the connector picks up route updates from Cloudflare.
Security model#
- Outbound only —
cloudflaredinitiates connections to Cloudflare; nothing listens on the WAN. - Token scope — The tunnel token grants access only to that tunnel’s configuration. Rotate it in Zero Trust if leaked.
- Split identity and network — Dex and similar apps use the tunnel for browser login (OIDC). The Kubernetes API server stays on a private path (VPN/LAN). See Dex OIDC.
Troubleshooting#
| Symptom | Check |
|---|---|
Pod CrashLoopBackOff | Invalid or expired tunnel token in cloudflare-tunnel-token |
| Hostname 502 / error | Public Hostname origin URL wrong, or target Service/pod not running |
| Connector not in dashboard | Pod logs; cluster egress to Cloudflare; token matches the tunnel |
kubectl -n cloudflare-tunnel describe pod -l app=cloudflared
kubectl -n cloudflare-tunnel logs deployment/cloudflared --tail=100See also#
- GitOps repositories —
homelabs/cloudflare/manifest layout - Dex OIDC — OIDC broker behind the tunnel
- Argo CD — GitOps UI hostname