Cloudflare Tunnel
Expose your hub to the internet without a public IP using Cloudflare Tunnel.
Table of contents
- Overview
- Prerequisites
- Step 1: Install cert-manager
- Step 2: Configure DNS Validation
- Step 3: Install Cloudflare Tunnel Controller
- Step 4: Deploy the Hub
- Step 5: Verify
- Exposing Additional Services
- Troubleshooting
- Reference
Overview
Cloudflare Tunnel creates a secure outbound connection from your cluster to Cloudflare’s edge network. This is the recommended approach for home labs.
Why Cloudflare Tunnel?
| Challenge | Solution |
|---|---|
| No public IP | Tunnel connects outbound — no inbound ports needed |
| Dynamic IP | DNS managed automatically by Cloudflare |
| NAT/CGNAT | Works through any NAT configuration |
| TLS certificates | Free certs via Let’s Encrypt + DNS validation |
| DDoS protection | Built into Cloudflare’s edge |
| Security | No exposed ports on your network |
How It Works
Remote Agent → Cloudflare Edge ← Tunnel Pod (your cluster)
↓
Your Hub Service
- A tunnel pod runs in your cluster
- It establishes an outbound connection to Cloudflare
- Cloudflare routes traffic through the tunnel to your services
- No inbound firewall rules needed
Prerequisites
| Requirement | Description |
|---|---|
| Cloudflare account | Free tier works |
| Domain on Cloudflare | Your domain’s DNS must be managed by Cloudflare |
| API token | With Cloudflare Tunnel:Edit and DNS:Edit permissions |
| Account ID | Found in the Cloudflare dashboard sidebar |
Create a Cloudflare API Token
- Go to Cloudflare Dashboard → My Profile → API Tokens
- Click Create Token
- Select Create Custom Token
- Configure permissions:
Account→Cloudflare Tunnel→EditZone→DNS→Edit
- Set the zone to your domain (or All zones)
- Click Continue to summary → Create Token
- Copy and save the token — you won’t see it again
Find Your Account ID
- Go to Cloudflare Dashboard
- Select any domain
- Look in the right sidebar under API → Account ID
- Copy the ID
Step 1: Install cert-manager
cert-manager issues TLS certificates for your hub. We use DNS01 validation via Cloudflare, which works even before your service is publicly accessible.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.0/cert-manager.yaml
Wait for it to be ready:
kubectl -n cert-manager wait --for=condition=ready pod -l app.kubernetes.io/instance=cert-manager --timeout=120s
Step 2: Configure DNS Validation
Create a secret with your Cloudflare API token:
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token="YOUR_CLOUDFLARE_API_TOKEN"
Create a ClusterIssuer for Let’s Encrypt:
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
EOF
Verify the issuer is ready:
kubectl get clusterissuer letsencrypt-prod
# NAME READY AGE
# letsencrypt-prod True 30s
DNS01 challenges work by creating TXT records via the Cloudflare API. No public access to your service is required during certificate issuance.
Step 3: Install Cloudflare Tunnel Controller
The tunnel ingress controller manages tunnels and DNS records automatically.
helm repo add strrl.dev https://helm.strrl.dev
helm repo update
Install the controller:
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="kedge-tunnel"
Verify it’s running:
kubectl -n cloudflare-tunnel-ingress-controller get pods
# NAME READY STATUS RESTARTS AGE
# cloudflare-tunnel-ingress-controller-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
Check the tunnel in Cloudflare Zero Trust:
- Go to Networks → Tunnels
- You should see
kedge-tunnelwith status Healthy
Step 4: Deploy the Hub
Create a values file for your hub (values-cloudflare.yaml):
One can generate random token with openssl rand -hex 16
hub:
hubExternalURL: "https://hub.faros.sh"
devMode: false
# Authentication - choose one:
staticAuthToken: "f782d49d86e73e5b9cfceb79ac2720ce" # Simple option
# OR use OIDC (see Security docs)
tls:
selfSigned:
enabled: false
certManager:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "hub.faros.sh"
ingress:
enabled: true
className: "cloudflare-tunnel"
hosts:
- host: hub.faros.sh
paths:
- path: /
pathType: ImplementationSpecific
Deploy:
helm upgrade --install kedge oci://ghcr.io/faroshq/charts/kedge-hub \
-f values.yaml \
--namespace kedge-system \
--create-namespace
Set --set image.hub.tag=v0.0.1 to override image
Step 5: Verify
Check certificate status
kubectl -n kedge-system get certificate
# NAME READY SECRET AGE
# kedge-kedge-hub-tls True kedge-kedge-hub-tls 2m
If not ready, check the certificate request:
kubectl -n kedge-system describe certificaterequest
Check ingress status
kubectl get ingress -n kedge-system
# NAME CLASS HOSTS ADDRESS PORTS AGE
# kedge-kedge-hub cloudflare-tunnel hub.yourdomain.com xxxx.cfargotunnel.com 80, 443 5m
Test connectivity
curl -s https://hub.faros.sh/healthz
# ok
Log in
kedge login --hub-url https://hub.yourdomain.com
Exposing Additional Services
Dex (OIDC Identity Provider)
If using OIDC authentication, Dex also needs to be publicly accessible. Add to your Dex values:
ingress:
enabled: true
className: "cloudflare-tunnel"
hosts:
- host: idp.yourdomain.com
paths:
- path: /
pathType: ImplementationSpecific
Both services share the same tunnel — just different hostnames. The tunnel controller automatically creates DNS records for each.
Multiple Hostnames
You can expose any number of services through the same tunnel:
# Service A
ingress:
className: "cloudflare-tunnel"
hosts:
- host: service-a.yourdomain.com
# Service B
ingress:
className: "cloudflare-tunnel"
hosts:
- host: service-b.yourdomain.com
Troubleshooting
Tunnel not connecting
Check the controller logs:
kubectl -n cloudflare-tunnel-ingress-controller logs -l app.kubernetes.io/name=cloudflare-tunnel-ingress-controller
Common issues:
- Invalid API token — Regenerate with correct permissions
- Wrong account ID — Double-check in dashboard
- Tunnel name conflict — Delete stale tunnels from the Cloudflare dashboard
Certificate not issuing
Check cert-manager logs:
kubectl -n cert-manager logs -l app=cert-manager
Check certificate status:
kubectl -n kedge-system describe certificate
kubectl -n kedge-system get certificaterequest,order,challenge
Common issues:
- API token missing DNS:Edit — Recreate with correct permissions
- Wrong zone — Token must have access to the domain’s zone
DNS not resolving
The tunnel controller creates CNAME records automatically. Verify:
dig hub.yourdomain.com CNAME
# Should return: xxxx.cfargotunnel.com
If missing, check the controller logs and ensure the ingress has an ADDRESS assigned.
Slow certificate issuance
DNS propagation can take a few minutes. If stuck:
- Check if the TXT record exists:
dig _acme-challenge.hub.yourdomain.com TXT - For kind clusters on macOS, DNS resolution can be slow. Apply this workaround:
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"}]'
Reference
Cloudflare Resources
Related Guides
- Security — Configure authentication
- Helm Deployment — Full Helm values reference