Complete Setup Guide

πŸ“…May 23, 2026
🏷️Infrastructure Security
⏱️25 min

This guide shows the exact steps I took to implement Cloudflare Zero Trust on my 4-node Kubernetes cluster. All commands, configurations, and screenshots are from my actual production deployment.


My Environment

Cluster:

  • Master: 192.0.2.10 (EU-Central) - Where cloudflared runs
  • Worker1: 192.0.2.11 (EU-Central)
  • Worker2: 192.0.2.12 (EU-Central)
  • Worker3: 192.0.2.13 (EU-North)

Services:

  • nginx-ingress: LoadBalancer on 192.0.2.10:80
  • ArgoCD: internal1.example.com
  • Harbor: internal2.example.com
  • Grafana: internal3.example.com
  • PWA: app.example.com
  • Landing: example.com

Phase 1: Cloudflare Tunnel Setup

Step 1: Install cloudflared on Master Node

SSH to the master node and install cloudflared:

ssh root@192.0.2.10
 
# Install cloudflared
apt-get update
apt-get install cloudflared -y
 
# Verify installation
cloudflared --version

My output:

cloudflared version 2026.5.0 (built 2026-05-15-1423 UTC)

Step 2: Create Tunnel via Dashboard

I chose the dashboard method (instead of CLI) because it’s easier for token-based authentication.

  • Go to https://one.dash.cloudflare.com
  • Navigate to: Networks β†’ Tunnels
  • Click Create a tunnel
  • Choose: Cloudflared
  • Name it: example-cluster
  • Click Save tunnel

Result: Tunnel created with ID aaaabbbb-cccc-dddd-eeee-ffff00001111


Step 3: Install Tunnel on Server

The dashboard shows installation command. Copy the token and run:

# This creates systemd service and starts tunnel
cloudflared service install EXAMPLE_TOKEN_BASE64_ENCODED_STRING_REPLACE_WITH_YOUR_TUNNEL_TOKEN

This command:

  • Creates /etc/systemd/system/cloudflared.service
  • Creates /root/.cloudflared/ directory with credentials
  • Starts cloudflared service
  • Enables it to start on boot

Step 4: Create Configuration File

Create /etc/cloudflared/config.yml with all domain mappings:

mkdir -p /etc/cloudflared
nano /etc/cloudflared/config.yml

My configuration:

tunnel: example-cluster
credentials-file: /root/.cloudflared/aaaabbbb-cccc-dddd-eeee-ffff00001111.json
 
ingress:
  # Public services
  - hostname: example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: example.com
 
  - hostname: app.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: app.example.com
 
  # Private services (will add Access later)
  - hostname: internal1.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: internal1.example.com
 
  - hostname: internal2.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: internal2.example.com
 
  - hostname: internal3.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: internal3.example.com
 
  # Catch-all
  - service: http_status:404
 
loglevel: info
logfile: /var/log/cloudflared.log
metrics: 0.0.0.0:2000
⚠️

Critical: Use http://192.0.2.10:80 NOT http://localhost:80

I initially used localhost and got 500 errors because nginx-ingress LoadBalancer listens on the external IP, not localhost!

Why these settings:

  • service: http://192.0.2.10:80 - nginx-ingress LoadBalancer external IP
  • noTLSVerify: true - nginx-ingress uses self-signed certs
  • httpHostHeader - nginx-ingress needs this to route to correct Ingress
  • metrics: 0.0.0.0:2000 - Exposes Prometheus metrics for monitoring

Step 5: Restart cloudflared

Apply the configuration:

systemctl restart cloudflared
systemctl status cloudflared

Expected output:

● cloudflared.service - Cloudflare Tunnel
   Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled)
   Active: active (running) since Fri 2026-05-23 08:15:32 UTC; 1min ago

Step 6: Verify Tunnel Connections

Check that tunnel has 4 HA connections:

curl http://localhost:2000/metrics | grep cloudflared_tunnel_ha_connections

My output:

cloudflared_tunnel_ha_connections 4

βœ… 4 connections active to EU-Central data centers


Phase 2: DNS Configuration

Step 1: Update DNS Records

For each domain, I needed to change from A records to CNAME records pointing to the tunnel.

In Cloudflare Dashboard:

  • Go to DNS β†’ Records

  • Delete existing A records for:

    • example.com
    • app.example.com
    • internal1.example.com
    • internal2.example.com
    • internal3.example.com
  • Create CNAME records:

TypeNameTargetProxy
CNAMEexample.comaaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.comβœ… Proxied
CNAMEappaaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.comβœ… Proxied
CNAMEargocdaaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.comβœ… Proxied
CNAMEharboraaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.comβœ… Proxied
CNAMEmonitoringaaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.comβœ… Proxied
🚫

Common mistake: I initially had A records pointing to 192.0.2.10, which bypassed Cloudflare! CNAMEs to the tunnel are required.


Step 2: Verify DNS Propagation

# Check DNS resolution
dig +short example.com @1.1.1.1
dig +short internal1.example.com @1.1.1.1

Expected: Cloudflare IPs (like 188.114.96.3, 188.114.97.3) NOT: Your server IP (192.0.2.10)


Step 3: Test Public Services

curl -I https://example.com
curl -I https://app.example.com

Look for:

HTTP/2 200
server: cloudflare
cf-ray: a00a00c5fce316f0-FRA

βœ… Traffic is routing through Cloudflare!


Phase 3: Cloudflare Access for Private Services

Step 1: Create Access Application (ArgoCD)

  • Go to Access β†’ Applications
  • Click Add an application
  • Choose Self-hosted

Configuration:

  • Application name: argocd
  • Session Duration: 24 hours
  • Application domain: internal1.example.com

Step 2: Create Access Policy

Policy name: Admin Access

Include rule:

  • Selector: Emails
  • Value: your@email.com (your email)

Authentication method:

  • β˜‘ One-time PIN

Click Add application


Step 3: Repeat for Harbor and Grafana

Create applications for:

  • harbor β†’ internal2.example.com
  • monitoring β†’ internal3.example.com

Same policy: Email OTP for admin email.


Step 4: Test Access Authentication

Open https://internal1.example.com in a browser:

  • You should see Cloudflare Access login page
  • Enter your email
  • Receive OTP code
  • Enter code
  • Access granted for 24 hours

βœ… Private services now require authentication!


Step 5: Verify Access Logs

Go to Access β†’ Logs β†’ Access requests

You should see:

  • Access events logged
  • Email addresses
  • Timestamps
  • Allow/Deny decisions

Phase 4: WARP for Infrastructure Access

Step 1: Install WARP Client

On your laptop/desktop:

  • macOS: Download from https://1.1.1.1/ or brew install --cask cloudflare-warp
  • Windows: Download from https://1.1.1.1/
  • Linux: curl https://pkg.cloudflareclient.com/install | sudo bash

Step 2: Enroll WARP with Your Organization

  • Open WARP client
  • Click Settings β†’ Preferences β†’ Account
  • Click Login to Cloudflare Zero Trust
  • Enter your organization name: goalixa-zero-trust (or your org name)
  • Authenticate with your email

WARP is now enrolled with your Zero Trust organization.


Step 3: Configure Split Tunnel

In the Cloudflare Dashboard:

  • Go to Settings β†’ WARP Client
  • Click Device settings β†’ Default
  • Scroll to Split Tunnels
  • Change mode to: Include IPs and domains
  • Add your cluster IPs:
192.0.2.10/32    # Master node
192.0.2.11/32     # Worker1
192.0.2.12/32    # Worker2
192.0.2.13/32  # Worker3

What this does:

  • Only cluster IPs route through WARP
  • All other traffic (google.com, etc.) goes direct
  • Minimizes WARP overhead for non-cluster traffic

Step 4: Enable Gateway

  • Go to Gateway β†’ Firewall Policies β†’ Network
  • Ensure Gateway is enabled
  • Go to Settings β†’ Network β†’ Proxy
  • Enable: β˜‘ Proxy
  • Enable: β˜‘ Protocol Detection
  • Protocols: β˜‘ TCP β˜‘ UDP β˜‘ ICMP

Step 5: Create Gateway Firewall Policies

Create two policies for cluster access:

Policy 1: Allow kubectl

  • Name: Allow kubectl to Kubernetes Cluster
  • Traffic:
    • Destination IP: 192.0.2.10
    • Destination Port: 6443
    • Protocol: TCP
  • Action: Allow
  • Logging: Enabled

Policy 2: Allow SSH

  • Name: Allow SSH to Kubernetes Cluster
  • Traffic:
    • Destination IPs: 192.0.2.10, 192.0.2.11, 192.0.2.12, 192.0.2.13
    • Destination Port: 22
    • Protocol: TCP
  • Action: Allow
  • Logging: Enabled

Step 6: Test kubectl Access

On your laptop with WARP connected:

# Connect WARP
warp-cli connect
 
# Wait for connection
warp-cli status
 
# Test kubectl
kubectl get pods -n default

It should work!

Disconnect WARP and try again:

warp-cli disconnect
kubectl get pods -n default

For kubectl (port 6443), it should still work because we haven’t closed the port yet.


Step 7: Verify Gateway Logs

Go to Logs β†’ Gateway β†’ Network

You should see:

  • Traffic to 192.0.2.10:6443 (kubectl)
  • Traffic to cluster IPs:22 (SSH)
  • Action: Allow
  • Your device identity

βœ… WARP is routing cluster traffic through Gateway!


Phase 5: Device Information

Check your enrolled device:

Go to My Team β†’ Devices

You can see:

  • Device name
  • OS and version
  • WARP client version
  • Network information (IP, location)
  • Last seen timestamp
  • Posture status

Verification Checklist

After completing all phases, verify everything works:

βœ… Tunnel

# On master node
curl http://localhost:2000/metrics | grep cloudflared_tunnel_ha_connections
 
# Should show: cloudflared_tunnel_ha_connections 4

βœ… Public Services

curl -I https://example.com
curl -I https://app.example.com
 
# Should see:
# HTTP/2 200
# server: cloudflare

βœ… Private Services (Access)

Open in browser:

Expected: Cloudflare Access login page, not direct service login.


βœ… kubectl (WARP)

# Connect WARP
warp-cli connect
 
# Test kubectl
kubectl get nodes
 
# Should work! Verify in Gateway logs.

βœ… SSH (WARP)

# With WARP connected
ssh root@192.0.2.10
 
# Should work! Check Gateway network logs.

Configuration Files Reference

/etc/cloudflared/config.yml

tunnel: example-cluster
credentials-file: /root/.cloudflared/aaaabbbb-cccc-dddd-eeee-ffff00001111.json
 
ingress:
  - hostname: example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: example.com
 
  - hostname: app.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: app.example.com
 
  - hostname: internal1.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: internal1.example.com
 
  - hostname: internal2.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: internal2.example.com
 
  - hostname: internal3.example.com
    service: http://192.0.2.10:80
    originRequest:
      noTLSVerify: true
      connectTimeout: 30s
      httpHostHeader: internal3.example.com
 
  - service: http_status:404
 
loglevel: info
logfile: /var/log/cloudflared.log
metrics: 0.0.0.0:2000

/etc/systemd/system/cloudflared.service

[Unit]
Description=Cloudflare Tunnel
After=network.target
 
[Service]
Type=simple
User=root
ExecStart=/usr/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run --token EXAMPLE_TOKEN_BASE64_ENCODED_STRING_REPLACE_WITH_YOUR_TUNNEL_TOKEN
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
 
[Install]
WantedBy=multi-user.target

WARP Split Tunnel Configuration

Mode: Include IPs and domains

Included IPs:

192.0.2.10/32
192.0.2.11/32
192.0.2.12/32
192.0.2.13/32

Gateway Firewall Policies

Policy: Allow kubectl

  • Destination IP: 192.0.2.10
  • Port: 6443
  • Protocol: TCP
  • Action: Allow

Policy: Allow SSH

  • Destination IPs: All 4 cluster nodes
  • Port: 22
  • Protocol: TCP
  • Action: Allow

Troubleshooting Common Issues

500 Internal Server Error

Problem: ArgoCD/Harbor show 500 errors after tunnel setup.

Cause: Config uses localhost:80 but nginx-ingress listens on external IP.

Fix: Change all service: lines in config.yml from http://localhost:80 to http://192.0.2.10:80


DNS Not Resolving to Cloudflare

Problem: dig internal1.example.com returns 192.0.2.10 instead of Cloudflare IP.

Cause: Still using A records instead of CNAME to tunnel.

Fix: Delete A records, create CNAME records pointing to TUNNEL_ID.cfargotunnel.com


WARP Not Connecting

Problem: WARP shows β€œUnable to connect”

Cause: Not enrolled with Zero Trust organization.

Fix: Settings β†’ Account β†’ Login to Cloudflare Zero Trust β†’ Enter org name


kubectl Not Working via WARP

Problem: kubectl fails when WARP connected.

Cause: Split tunnel not including cluster IP.

Fix: Add 192.0.2.10/32 to Split Tunnel include list


Next Steps

Implementation complete! Now check performance and metrics:

Continue to Performance & Metrics β†’