Deploying Cloudflare Tunnels in Kubernetes for Secure Application Access
Cloudflare Tunnels provide a secure way to expose your internal applications to the internet without opening inbound ports or exposing your infrastructure. This guide demonstrates how to deploy Cloudflare Tunnels in Kubernetes to securely access applications like GitLab.
Overview
Cloudflare Tunnel (formerly Argo Tunnel) creates an encrypted tunnel between your origin server and Cloudflare’s edge network. This approach offers several security advantages:
- No Public IPs Required: Applications remain completely private
- Zero-Trust Security: All traffic is authenticated and encrypted
- DDoS Protection: Leverages Cloudflare’s global network
- Easy Management: Centralized configuration through Cloudflare dashboard
Prerequisites
- Kubernetes cluster (1.19+)
- kubectl configured
- Cloudflare account with a domain
- Cloudflare Tunnel token
Security-First Approach
Before diving into the deployment, let’s address the security considerations upfront.
1. Secure Token Storage
Never store Cloudflare tokens in plain text. Always use Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: gitlab-cloudflared-token
namespace: default
type: Opaque
stringData:
token: eyJhIjoiNjcwODgwNDJhYjYyYzAwZTU0MjAwOTRlZjIwZTkyNDQiLCJ0IjoiNjEyMmU5ZjItNTBhYS00M2ExLTk4YzYtNDYyYmEyYzU1OGFmIiwicyI6IlhZL1piNGxGVHZDNVZtL2RnVW5lSUQvSDNQNHpXNXRiNHpCSVB1aHF6b3M9In0=
2. RBAC Configuration
Create appropriate service accounts and role bindings:
apiVersion: v1
kind: ServiceAccount
metadata:
name: cloudflared
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cloudflared
namespace: default
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["gitlab-cloudflared-token"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cloudflared
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cloudflared
subjects:
- kind: ServiceAccount
name: cloudflared
namespace: default
Complete Deployment Configuration
Here’s the production-ready deployment for GitLab with Cloudflare Tunnel:
apiVersion: v1
kind: Secret
metadata:
name: gitlab-cloudflared-token
namespace: default
type: Opaque
stringData:
token: YOUR_CLOUDFLARE_TUNNEL_TOKEN_HERE
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cloudflared-config
namespace: default
data:
config.yaml: |
tunnel: gitlab-tunnel
credentials-file: /etc/cloudflared/creds/credentials.json
metrics: 0.0.0.0:2000
no-autoupdate: true
ingress:
- hostname: gitlab.yourdomain.com
service: http://gitlab-service:80
- service: http_status:404
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitlab-cloudflared-deployment
namespace: default
labels:
app: gitlab-cloudflared
spec:
replicas: 2
selector:
matchLabels:
pod: cloudflared
app: gitlab-cloudflared
template:
metadata:
labels:
pod: cloudflared
app: gitlab-cloudflared
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "2000"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: cloudflared
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
containers:
- name: cloudflared
image: cloudflare/cloudflared:2024.1.5
command:
- cloudflared
- tunnel
- --no-autoupdate
- --metrics
- 0.0.0.0:2000
- run
args:
- --token
- $(TUNNEL_TOKEN)
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: gitlab-cloudflared-token
key: token
- name: TUNNEL_LOGLEVEL
value: "info"
- name: TUNNEL_TRANSPORT_PROTOCOL
value: "quic"
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
livenessProbe:
httpGet:
path: /ready
port: 2000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 1
readinessProbe:
httpGet:
path: /ready
port: 2000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: config
mountPath: /etc/cloudflared
readOnly: true
volumes:
- name: config
configMap:
name: cloudflared-config
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- gitlab-cloudflared
topologyKey: kubernetes.io/hostname
---
apiVersion: v1
kind: Service
metadata:
name: cloudflared-metrics
namespace: default
labels:
app: gitlab-cloudflared
spec:
ports:
- name: metrics
port: 2000
targetPort: 2000
selector:
app: gitlab-cloudflared
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: gitlab-cloudflared-pdb
namespace: default
spec:
minAvailable: 1
selector:
matchLabels:
app: gitlab-cloudflared
Advanced Configuration Options
1. Multiple Services Through Single Tunnel
Configure multiple services through one tunnel:
data:
config.yaml: |
tunnel: multi-service-tunnel
credentials-file: /etc/cloudflared/creds/credentials.json
ingress:
- hostname: gitlab.yourdomain.com
service: http://gitlab-service:80
- hostname: jenkins.yourdomain.com
service: http://jenkins-service:8080
- hostname: grafana.yourdomain.com
service: http://grafana-service:3000
- service: http_status:404
2. Origin Server Configuration
Configure advanced origin settings:
data:
config.yaml: |
tunnel: gitlab-tunnel
ingress:
- hostname: gitlab.yourdomain.com
service: http://gitlab-service:80
originRequest:
connectTimeout: 30s
tlsTimeout: 10s
tcpKeepAlive: 30s
noHappyEyeballs: false
keepAliveConnections: 100
keepAliveTimeout: 90s
httpHostHeader: gitlab.internal.local
originServerName: gitlab.internal.local
caPool: /etc/cloudflared/ca.crt
noTLSVerify: false
disableChunkedEncoding: false
proxyAddress: 127.0.0.1
proxyPort: 0
proxyType: ""
- service: http_status:404
3. Load Balancing Configuration
For high-traffic applications, implement load balancing:
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitlab-cloudflared-deployment
spec:
replicas: 3 # Increase replicas for load distribution
template:
spec:
containers:
- name: cloudflared
env:
- name: TUNNEL_EDGE_IP_VERSION
value: "auto" # Use both IPv4 and IPv6
- name: TUNNEL_PROTOCOL
value: "quic" # Use QUIC for better performance
- name: TUNNEL_RETRIES
value: "5"
- name: TUNNEL_GRACE_PERIOD
value: "30s"
Monitoring and Observability
1. Prometheus Metrics
The Cloudflared deployment exposes metrics on port 2000. Configure Prometheus to scrape these metrics:
apiVersion: v1
kind: ServiceMonitor
metadata:
name: cloudflared-metrics
namespace: default
spec:
selector:
matchLabels:
app: gitlab-cloudflared
endpoints:
- port: metrics
interval: 30s
path: /metrics
2. Key Metrics to Monitor
cloudflared_tunnel_active_streams
: Number of active connectionscloudflared_tunnel_request_errors
: Request error ratecloudflared_tunnel_response_time
: Response time histogramcloudflared_tunnel_concurrent_requests
: Concurrent request count
3. Logging Configuration
Configure structured logging for better observability:
env:
- name: TUNNEL_LOGLEVEL
value: "info"
- name: TUNNEL_LOGFILE
value: "/dev/stdout"
- name: TUNNEL_LOG_FORMAT
value: "json"
Security Best Practices
1. Network Policies
Implement strict network policies:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cloudflared-network-policy
namespace: default
spec:
podSelector:
matchLabels:
app: gitlab-cloudflared
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
monitoring: prometheus
ports:
- protocol: TCP
port: 2000
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443 # Cloudflare API
- protocol: UDP
port: 7844 # QUIC
- to:
- podSelector:
matchLabels:
app: gitlab
ports:
- protocol: TCP
port: 80
2. Pod Security Standards
Apply pod security standards:
apiVersion: v1
kind: Namespace
metadata:
name: cloudflare-tunnels
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
3. Secret Rotation
Implement automatic token rotation:
#!/bin/bash
# Rotate Cloudflare tunnel token
# Generate new token via Cloudflare API
NEW_TOKEN=$(cloudflared tunnel token ${TUNNEL_NAME})
# Update Kubernetes secret
kubectl create secret generic gitlab-cloudflared-token \
--from-literal=token=${NEW_TOKEN} \
--dry-run=client -o yaml | kubectl apply -f -
# Restart deployment to pick up new token
kubectl rollout restart deployment/gitlab-cloudflared-deployment
Troubleshooting Guide
1. Connection Issues
Check tunnel status:
# Check pod logs
kubectl logs -l app=gitlab-cloudflared -f
# Verify tunnel connectivity
kubectl exec -it deployment/gitlab-cloudflared-deployment -- cloudflared tunnel info
# Test service connectivity
kubectl exec -it deployment/gitlab-cloudflared-deployment -- curl -I http://gitlab-service:80
2. Common Error Messages
“Unable to reach the origin service”:
- Verify service name and port
- Check network policies
- Ensure origin service is running
“Tunnel credentials file not found”:
- Verify secret is properly mounted
- Check token format and encoding
- Ensure proper RBAC permissions
“Failed to serve quic connection”:
- Check firewall rules for UDP port 7844
- Try fallback to HTTP2:
TUNNEL_TRANSPORT_PROTOCOL=http2
3. Performance Optimization
For high-traffic scenarios:
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
env:
- name: TUNNEL_COMPRESSION_LEVEL
value: "0" # Disable compression for low-latency
- name: TUNNEL_TCP_KEEPALIVE
value: "30"
- name: TUNNEL_KEEP_ALIVE_CONNECTIONS
value: "100"
Integration with CI/CD
GitLab CI/CD Pipeline
Automate tunnel deployment with GitLab CI:
stages:
- deploy
deploy-tunnel:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl apply -f cloudflare-tunnel.yaml
- kubectl rollout status deployment/gitlab-cloudflared-deployment
only:
- main
environment:
name: production
ArgoCD Application
Deploy using GitOps with ArgoCD:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cloudflare-tunnel
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/yourorg/k8s-configs
targetRevision: HEAD
path: cloudflare-tunnels
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
Cost Optimization
1. Resource Right-Sizing
Monitor actual resource usage and adjust:
# Get resource usage
kubectl top pod -l app=gitlab-cloudflared
# Adjust resources based on actual usage
2. Autoscaling
Implement horizontal pod autoscaling:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: cloudflared-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gitlab-cloudflared-deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Conclusion
Cloudflare Tunnels provide a secure, scalable solution for exposing internal applications without compromising security. This deployment configuration ensures:
- High Availability: Multiple replicas with pod anti-affinity
- Security: Least privilege, encrypted secrets, network policies
- Observability: Comprehensive metrics and logging
- Performance: Optimized resource usage and QUIC protocol
- Maintainability: GitOps-ready configuration
By following this guide, you can securely expose your applications while maintaining a zero-trust security posture and leveraging Cloudflare’s global network for performance and protection.