Cloudflare Tunnel¶
This section describes how Cloudflare Tunnel is used to expose cluster services to the internet without any public IP, open port, or LoadBalancer.
All external traffic enters the cluster through a single secure outbound connection
established by the cloudflared agent running inside the cluster.
Architecture¶
- TLS is terminated at the Cloudflare edge — traffic inside the cluster is plain HTTP
- No inbound ports are opened on the cluster nodes
- One tunnel handles all services via hostname-based routing
How It Works¶
The cloudflared agent runs inside the cluster and maintains a persistent
outbound connection to the Cloudflare edge network.
When a user requests https://argocd.kanismile.com:
- Cloudflare resolves the domain and routes the request through the tunnel
cloudflaredreceives the request inside the cluster- It forwards the request to
edge-gateway-nginx.nginx-gateway.svc.cluster.local:80 - NGINX Gateway Fabric matches the hostname via the corresponding HTTPRoute
- The request is forwarded to the target service
Tunnel Configuration¶
The tunnel was created via the Cloudflare Zero Trust dashboard and configured with one public hostname per service, all pointing to the same internal service:
| Hostname | Internal Service |
|---|---|
argocd.kanismile.com |
http://edge-gateway-nginx.nginx-gateway.svc.cluster.local:80 |
grafana.kanismile.com |
http://edge-gateway-nginx.nginx-gateway.svc.cluster.local:80 |
prometheus.kanismile.com |
http://edge-gateway-nginx.nginx-gateway.svc.cluster.local:80 |
alertmanager.kanismile.com |
http://edge-gateway-nginx.nginx-gateway.svc.cluster.local:80 |
Info
All hostnames point to the same internal URL. Hostname-based routing is handled by the HTTPRoutes at the Gateway layer — not by the tunnel itself.
Deployment¶
The cloudflared agent runs as a Deployment in the cloudflare namespace.
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflare
spec:
replicas: 1
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- run
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: cloudflared-token
key: TUNNEL_TOKEN
resources:
limits:
memory: "128Mi"
cpu: "100m"
Apply it with:
kubectl create namespace cloudflare
kubectl apply -f cloudflare/cloudflared.yaml
Tunnel Token Secret¶
The tunnel token is stored as a Kubernetes Secret and injected via environment variable.
Create the secret from the token provided by the Cloudflare Zero Trust dashboard:
kubectl create secret generic cloudflared-token \
--from-literal=TUNNEL_TOKEN=<your-tunnel-token> \
--namespace cloudflare
Warning
Never commit the tunnel token to Git. The secret must be created manually on the cluster before applying the Deployment.
Verify the Deployment¶
kubectl get pods -n cloudflare
Expected output:
NAME READY STATUS RESTARTS AGE
cloudflared-xxxx 1/1 Running 0 ...
Check the tunnel is connected from the Cloudflare Zero Trust dashboard:
Zero Trust → Networks → Tunnels → k8s-homelab → Status: Healthy
Design Decisions¶
- Single tunnel for all services — one
cloudflaredinstance handles all hostnames - All routes point to the Gateway — routing logic stays in Kubernetes (HTTPRoutes), not in Cloudflare
- Token via Secret — the tunnel token is never stored in Git
- No public IP required — the cluster nodes have no inbound exposure
Tip
To add a new service, simply create a new HTTPRoute in the cluster
and add the corresponding hostname in the Cloudflare Zero Trust dashboard
pointing to http://edge-gateway-nginx.nginx-gateway.svc.cluster.local:80.