Skip to main content

Cloudflare Tunnel

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:

HostnameBackendDoc
dex.<your-domain>Dex OIDC brokerDex OIDC
argocd.<your-domain>Argo CD UIArgo CD
immich.<your-domain>Immich serverImmich

Manifests live in the public homelabs repo under cloudflare/ (GitOps repositories).

Create the tunnel in Zero Trust
#

  1. Open Cloudflare Zero TrustNetworksTunnels.
  2. Create a tunnel → choose Cloudflared.
  3. Name the tunnel (for example homelab-k8s).
  4. On the Install connector step, choose Docker (or any option) and copy the tunnel token — you need it for the Kubernetes Secret below.
  5. 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.yaml

The 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/cloudflared

Healthy 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 (NetworksTunnels → your tunnel → Public Hostname).

FieldTypical value
SubdomainService name (dex, argocd, immich, …)
DomainYour public domain
TypeHTTP for plain in-cluster HTTP; HTTPS when the Service speaks TLS
URLIn-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:443 with 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 onlycloudflared initiates 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
#

SymptomCheck
Pod CrashLoopBackOffInvalid or expired tunnel token in cloudflare-tunnel-token
Hostname 502 / errorPublic Hostname origin URL wrong, or target Service/pod not running
Connector not in dashboardPod 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=100

See also
#