Complete Setup Guide
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 --versionMy 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_TOKENThis 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.ymlMy 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:2000Critical: 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 IPnoTLSVerify: true- nginx-ingress uses self-signed certshttpHostHeader- nginx-ingress needs this to route to correct Ingressmetrics: 0.0.0.0:2000- Exposes Prometheus metrics for monitoring
Step 5: Restart cloudflared
Apply the configuration:
systemctl restart cloudflared
systemctl status cloudflaredExpected 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 agoStep 6: Verify Tunnel Connections
Check that tunnel has 4 HA connections:
curl http://localhost:2000/metrics | grep cloudflared_tunnel_ha_connectionsMy 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:
| Type | Name | Target | Proxy |
|---|---|---|---|
| CNAME | example.com | aaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.com | β Proxied |
| CNAME | app | aaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.com | β Proxied |
| CNAME | argocd | aaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.com | β Proxied |
| CNAME | harbor | aaaabbbb-cccc-dddd-eeee-ffff00001111.cfargotunnel.com | β Proxied |
| CNAME | monitoring | aaaabbbb-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.1Expected: 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.comLook 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.commonitoringβ 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 # Worker3What 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
- Destination IP:
- 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
- Destination IPs:
- 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 defaultIt should work!
Disconnect WARP and try again:
warp-cli disconnect
kubectl get pods -n defaultFor 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.targetWARP 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/32Gateway 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: