Securing Kubernetes Access with Cloudflare Zero Trust

Cloudflare runs a significant amount of engineering workloads on Kubernetes, from API backends and batch-processing jobs to CI/CD pipelines. The default attack surface—load balancers, API servers, etcd, ingresses, and pods—is substantial. To reduce that exposure, the team tightly restricts network access to clusters, adding Cloudflare Access or mutual TLS (or both) on any exposed ingress.

Those restrictions extend to the Kubernetes API server itself. Blocking it outright would have been a non-starter: engineers need kubectl for troubleshooting and velocity, even when GitOps and Continuous Deployments are the norm. The solution the team landed on is Cloudflare Zero Trust private network routing, which lets engineers reach the API server without configuring proxies or tunnels on their devices.

From VPN to Tunnels to Zero Trust

Previously, engineers connected to the Kubernetes API through a VPN appliance. That worked but dropped them onto the internal network, granting far more access than needed. When Cloudflare retired its VPN in early 2020, the Kubernetes team had to find another path.

Working with the Cloudflare Tunnels team, they added support for kubectl connections through Access and cloudflared tunnels. That solved the immediate problem but created an onboarding burden: each cluster required its own tunnel connection from the engineer's machine, and switching between clusters was tedious. SOCKS proxy support in kubectl existed, but not every Kubernetes ecosystem tool supported it.

The next step was adopting the Zero Trust agent, initially for secure DNS with 1.1.1.1 and later for broader Zero Trust features. The team now uses private network routing to reach Kubernetes APIs directly—no per-cluster tunnel setup, no kubectl configuration changes.

BLOG-1190 Embedded Image - 0iJIrm

Configuring the Zero Trust Side

The Zero Trust configuration is managed as infrastructure-as-code, though the same settings can be applied through the Cloudflare Zero Trust dashboard. The first piece is a new tunnel that connects the Cloudflare edge to the Kubernetes API.

resource "cloudflare_argo_tunnel" "k8s_zero_trust_tunnel" {
  account_id = var.account_id
  name       = "k8s_zero_trust_tunnel"
  secret     = var.tunnel_secret
}

The tunnel_secret should be a 32-byte random number; save it temporarily because it will be reused during the Kubernetes deployment.

With the tunnel created, routes must be defined so the Cloudflare network knows which traffic to send through it.

resource "cloudflare_tunnel_route" "k8s_zero_trust_tunnel_ipv4" {
  account_id = var.account_id
  tunnel_id  = cloudflare_argo_tunnel.k8s_zero_trust_tunnel.id
  network    = "198.51.100.101/32"
  comment    = "Kubernetes API Server (IPv4)"
}
 
resource "cloudflare_tunnel_route" "k8s_zero_trust_tunnel_ipv6" {
  account_id = var.account_id
  tunnel_id  = cloudflare_argo_tunnel.k8s_zero_trust_tunnel.id
  network    = "2001:DB8::101/128"
  comment    = "Kubernetes API Server (IPv6)"
}

Both IPv4 and IPv6 are supported for API server access, so routes for both address families should be configured. If the API server is reached via hostname, these IPs should match DNS lookup results.

Next, Cloudflare Gateway settings need to align with the API servers and clients.

resource "cloudflare_teams_list" "k8s_apiserver_ips" {
  account_id = var.account_id
  name       = "Kubernetes API IPs"
  type       = "IP"
  items      = ["198.51.100.101/32", "2001:DB8::101/128"]
}
 
resource "cloudflare_teams_rule" "k8s_apiserver_zero_trust_http" {
  account_id  = var.account_id
  name        = "Don't inspect Kubernetes API"
  description = "Allow connections from kubectl to API"
  precedence  = 10000
  action      = "off"
  enabled     = true
  filters     = ["http"]
  traffic     = format("any(http.conn.dst_ip[*] in $%s)", replace(cloudflare_teams_list.k8s_apiserver_ips.id, "-", ""))
}

Since mutual TLS is used between clients and API servers, and traffic between kubectl and the API is not all HTTP, HTTP inspection is disabled for these connections. Additional Zero Trust rules—such as device attestation, session lifetimes, and user/group access policies—can be layered on for tighter security.

Deploying Tunnel Endpoints in Kubernetes

Tunnel endpoints are deployed as pods, which leverages Kubernetes deployment strategies for rolling upgrades and node failure handling. First, a ConfigMap is created with the minimal configuration for WARP routing using the tunnel ID.

apiVersion: v1
kind: ConfigMap
metadata:
  name: tunnel-zt
  namespace: example
  labels:
    tunnel: api-zt
data:
  config.yaml: |
    tunnel: 8e343b13-a087-48ea-825f-9783931ff2a5
    credentials-file: /opt/zt/creds/creds.json
    metrics: 0.0.0.0:8081
    warp-routing:
        enabled: true

The tunnel ID can be retrieved from the configuration management system, the Zero Trust dashboard, or by running:

cloudflared tunnel list

Next, a secret holds the tunnel credentials. Ideally this would come from a secret management system, but a direct creation works for simplicity.

jq -cn --arg accountTag $CF_ACCOUNT_TAG \
       --arg tunnelID $CF_TUNNEL_ID \
       --arg tunnelName $CF_TUNNEL_NAME \
       --arg tunnelSecret $CF_TUNNEL_SECRET \
   '{AccountTag: $accountTag, TunnelID: $tunnelID, TunnelName: $tunnelName, TunnelSecret: $tunnelSecret}' | \
kubectl create secret generic -n example tunnel-creds --from-file=creds.json=/dev/stdin

This creates a tunnel-creds secret in the example namespace containing the credentials file the tunnel expects. Multiple tunnel replicas are deployed to maintain availability during node drains.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    tunnel: api-zt
  name: tunnel-api-zt
  namespace: example
spec:
  replicas: 3
  selector:
    matchLabels:
      tunnel: api-zt
  strategy:
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
  template:
    metadata:
      labels:
        tunnel: api-zt
    spec:
      containers:
        - args:
            - tunnel
            - --config
            - /opt/zt/config/config.yaml
            - run
          env:
            - name: GOMAXPROCS
              value: "2"
            - name: TZ
              value: UTC
          image: cloudflare/cloudflared:2022.5.3
          livenessProbe:
            failureThreshold: 1
            httpGet:
              path: /ready
              port: 8081
            initialDelaySeconds: 10
            periodSeconds: 10
          name: tunnel
          ports:
            - containerPort: 8081
              name: http-metrics
          resources:
            limits:
              cpu: "1"
              memory: 100Mi
          volumeMounts:
            - mountPath: /opt/zt/config
              name: config
              readOnly: true
            - mountPath: /opt/zt/creds
              name: creds
              readOnly: true
      volumes:
        - secret:
            name: tunnel-creds
          name: creds
        - configMap:
            name: tunnel-api-zt
          name: config
Screenshot of the Cloudflare WARP app showing it connected to Teams

Once the Zero Trust agent is deployed, team members can reach the Kubernetes API directly—no SOCKS tunnels or special kubectl configuration required.

What's Next

Cloudflare is continuing to refine Zero Trust for non-HTTP workflows and welcomes feedback from teams who try this approach.