Skip to content

Deploying Cloudflare Tunnels in Kubernetes for Secure Application Access

Published: at 09:45 AM

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:

Prerequisites

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

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”:

“Tunnel credentials file not found”:

“Failed to serve quic connection”:

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:

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.