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: v1kind: Secretmetadata: name: gitlab-cloudflared-token namespace: defaulttype: OpaquestringData: token: eyJhIjoiNjcwODgwNDJhYjYyYzAwZTU0MjAwOTRlZjIwZTkyNDQiLCJ0IjoiNjEyMmU5ZjItNTBhYS00M2ExLTk4YzYtNDYyYmEyYzU1OGFmIiwicyI6IlhZL1piNGxGVHZDNVZtL2RnVW5lSUQvSDNQNHpXNXRiNHpCSVB1aHF6b3M9In0=
2. RBAC Configuration
Create appropriate service accounts and role bindings:
apiVersion: v1kind: ServiceAccountmetadata: name: cloudflared namespace: default---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: cloudflared namespace: defaultrules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["gitlab-cloudflared-token"] verbs: ["get"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: cloudflared namespace: defaultroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: cloudflaredsubjects: - kind: ServiceAccount name: cloudflared namespace: default
Complete Deployment Configuration
Here’s the production-ready deployment for GitLab with Cloudflare Tunnel:
apiVersion: v1kind: Secretmetadata: name: gitlab-cloudflared-token namespace: defaulttype: OpaquestringData: token: YOUR_CLOUDFLARE_TUNNEL_TOKEN_HERE---apiVersion: v1kind: ConfigMapmetadata: name: cloudflared-config namespace: defaultdata: 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/v1kind: Deploymentmetadata: name: gitlab-cloudflared-deployment namespace: default labels: app: gitlab-cloudflaredspec: 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: v1kind: Servicemetadata: name: cloudflared-metrics namespace: default labels: app: gitlab-cloudflaredspec: ports: - name: metrics port: 2000 targetPort: 2000 selector: app: gitlab-cloudflared---apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: gitlab-cloudflared-pdb namespace: defaultspec: 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/v1kind: Deploymentmetadata: name: gitlab-cloudflared-deploymentspec: 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: v1kind: ServiceMonitormetadata: name: cloudflared-metrics namespace: defaultspec: 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/v1kind: NetworkPolicymetadata: name: cloudflared-network-policy namespace: defaultspec: 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: v1kind: Namespacemetadata: 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 APINEW_TOKEN=$(cloudflared tunnel token ${TUNNEL_NAME})
# Update Kubernetes secretkubectl 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 tokenkubectl rollout restart deployment/gitlab-cloudflared-deployment
Troubleshooting Guide
1. Connection Issues
Check tunnel status:
# Check pod logskubectl logs -l app=gitlab-cloudflared -f
# Verify tunnel connectivitykubectl exec -it deployment/gitlab-cloudflared-deployment -- cloudflared tunnel info
# Test service connectivitykubectl 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/v1alpha1kind: Applicationmetadata: name: cloudflare-tunnel namespace: argocdspec: 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 usagekubectl top pod -l app=gitlab-cloudflared
# Adjust resources based on actual usage
2. Autoscaling
Implement horizontal pod autoscaling:
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: cloudflared-hpaspec: 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.