Cloudflare Tunnel
Expose a hub through an outbound tunnel with DNS and TLS configuration.
Cloudflare Tunnel opens an outbound connection from your cluster to Cloudflare’s edge. Cloudflare then routes inbound traffic to your hub through that tunnel — no inbound firewall rules, no public IP, no port forwards. The cluster must allow outbound tunnel traffic. Keep hub authentication enabled: a tunnel provides connectivity, not application authorization.
Why Cloudflare Tunnel
| Challenge | Cloudflare Tunnel solves it because |
|---|---|
| No public IP | Tunnel connects outbound — no inbound ports needed |
| Dynamic IP | DNS managed by Cloudflare automatically |
| NAT / CGNAT | Outbound connectivity avoids inbound port forwarding; restrictive egress still needs configuration |
| TLS certs | Free certs via Let’s Encrypt + DNS validation |
| Network exposure | No inbound port forwarding is required for this tunnel; audit other services separately |
Remote Agent → Cloudflare Edge ← Tunnel Pod (your cluster)
↓
Your Hub ServicePrerequisites
| Requirement | Notes |
|---|---|
| Cloudflare account | An account with access to the target zone and Tunnel configuration |
| Domain on Cloudflare | DNS for the domain must be managed by Cloudflare |
| API token | With Cloudflare Tunnel: Edit and DNS: Edit |
| Account ID | Found in the dashboard sidebar |
Create a Cloudflare API token
- Cloudflare Dashboard → My Profile → API Tokens → Create Token → Custom Token.
- Permissions:
Account→Cloudflare Tunnel→EditZone→DNS→Edit
- Restrict the token to the target account and the specific DNS zone used for this hub.
- Continue to summary → Create Token. Copy the token; you can’t see it again.
Find your account ID
Any domain page in the dashboard → right sidebar → API → Account ID.
Step 1 — Install cert-manager
cert-manager issues TLS certificates for the hub. We use DNS-01 validation via Cloudflare so cert issuance works even before the hub is publicly reachable.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.0/cert-manager.yaml
kubectl -n cert-manager wait --for=condition=ready pod \
-l app.kubernetes.io/instance=cert-manager --timeout=120sStep 2 — Configure DNS-01 validation
Store the API token as a secret in the cert-manager namespace:
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token="YOUR_CLOUDFLARE_API_TOKEN"Create a ClusterIssuer that uses it:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-tokenVerify:
kubectl get clusterissuer letsencrypt-prod
# letsencrypt-prod True 30sStep 3 — Choose a Cloudflare route integration
The hub chart supports both a Kubernetes Ingress and a Gateway API HTTPRoute. The commands below use the strrl.dev Ingress controller, so keep ingress.enabled: true in the hub values. If your cluster already runs Faros’s cfgate Cloudflare controller, skip this controller install and use the HTTPRoute values shown below instead; do not enable both integrations for the same hostname.
helm repo add strrl.dev https://helm.strrl.dev
helm repo update
helm upgrade --install --wait \
-n cloudflare-tunnel-ingress-controller --create-namespace \
cloudflare-tunnel-ingress-controller \
strrl.dev/cloudflare-tunnel-ingress-controller \
--set=cloudflare.apiToken="YOUR_CLOUDFLARE_API_TOKEN" \
--set=cloudflare.accountId="YOUR_CLOUDFLARE_ACCOUNT_ID" \
--set=cloudflare.tunnelName="faros-tunnel"In the Cloudflare Zero Trust dashboard
→ Networks → Tunnels you should see faros-tunnel as Healthy.
For the cfgate Gateway API integration, use this route block instead of the ingress block in the next step:
httpRoute:
enabled: true
parentRefs:
- name: cloudflare-tunnel
namespace: cfgate-system
hostnames:
- hub.yourdomain.com
annotations:
cfgate.io/origin-protocol: "https"
cfgate.io/origin-ssl-verify: "false" # the chart's hub origin is self-signed by defaultStep 4 — Deploy the hub
# values-cloudflare.yaml
hub:
hubExternalURL: "https://hub.yourdomain.com"
devMode: false
# Static tokens (list; each is its own tenant user) — or omit for OIDC
staticAuthTokens:
- "<openssl rand -hex 32>"
tls:
selfSigned:
enabled: false
certManager:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "hub.yourdomain.com"
ingress:
enabled: true
className: "cloudflare-tunnel"
hosts:
- host: hub.yourdomain.com
paths:
- path: /
pathType: ImplementationSpecifichelm upgrade --install faros oci://ghcr.io/faroshq/charts/faros-hub \
-f values-cloudflare.yaml \
--namespace faros-system \
--create-namespaceStep 5 — Verify
# TLS cert ready?
kubectl -n faros-system get certificate
# faros-faros-hub-tls True ...
# In the Ingress-controller variant, inspect the generated route.
kubectl get ingress -n faros-system
# End-to-end
curl -s https://hub.yourdomain.com/healthz
# ok
# Log in
kubectl faros login --hub-url https://hub.yourdomain.comExposing additional services through the same tunnel
The tunnel can carry multiple hostnames. If you’re running Dex for OIDC, give it the same ingressClassName:
ingress:
enabled: true
className: "cloudflare-tunnel"
hosts:
- host: idp.yourdomain.com
paths:
- path: /
pathType: ImplementationSpecificThe controller creates the CNAME automatically.
Troubleshooting
Tunnel not connecting
kubectl -n cloudflare-tunnel-ingress-controller logs \
-l app.kubernetes.io/name=cloudflare-tunnel-ingress-controllerCommon causes:
- Invalid API token — Re-create with
Cloudflare Tunnel: Edit+DNS: Edit - Wrong account ID — Double-check in the dashboard
- Tunnel name conflict — Delete stale tunnels from the Cloudflare dashboard
Certificate not issuing
kubectl -n cert-manager logs -l app=cert-manager
kubectl -n faros-system describe certificate
kubectl -n faros-system get certificaterequest,order,challengeMost failures here are missing DNS:Edit on the API token, or the token doesn’t cover the zone.
DNS not resolving
The ingress controller creates a CNAME pointing at xxxx.cfargotunnel.com. Check it propagated:
dig hub.yourdomain.com CNAMEkind on macOS — slow DNS-01 challenges
If cert-manager keeps re-trying the DNS-01 challenge on a kind cluster, force it to use public resolvers:
kubectl -n cert-manager patch deployment cert-manager --type=json -p='[
{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53"},
{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--dns01-recursive-nameservers-only"}
]'References
- Cloudflare Tunnel docs
- Cloudflare Zero Trust dashboard
- Tunnel ingress controller
- Security — pick an auth method for the hub