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: defaultComplete 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-cloudflaredAdvanced 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:4042. 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:4043. 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: /metrics2. 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: 802. 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: restricted3. 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-deploymentTroubleshooting 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:802. 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: productionArgoCD 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: trueCost 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 usage2. 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: 80Conclusion
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.