Cloudflare Tunnel (Argo) with K3s Cluster
This guide shows you how to securely expose your K3s services to the internet using Cloudflare Tunnel, without opening any firewall ports on your Proxmox host or VMs.
Architecture Overview
- Cloudflare Tunnel: Creates encrypted outbound connections from your cluster to Cloudflare’s edge
- No inbound ports needed: All traffic goes through the tunnel
- Services exposed: Rancher, web apps, APIs, etc.
- Authentication: Cloudflare Access can add SSO/Zero Trust
Part 1: Prerequisites
Step 1.1: Cloudflare Account Setup
- Sign up/Login at dash.cloudflare.com
- Add your domain (or use a free domain you own)
- Note your Account ID (from the URL or sidebar)
Step 1.2: Install cloudflared on Master Node
# On master node (k3s-1)
# Download cloudflared
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64
mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
# Verify installation
cloudflared --version
Part 2: Authenticate and Create Tunnel
Step 2.1: Authenticate with Cloudflare
# Login to Cloudflare (will open a URL)
cloudflared tunnel login
# Follow the URL, select your domain, and authenticate
# This creates a certificate file in ~/.cloudflared/
Step 2.2: Create a Tunnel
# Create a tunnel (give it a name, e.g., 'k3s-cluster')
cloudflared tunnel create k3s-cluster
# This creates:
# - Tunnel credentials file in ~/.cloudflared/
# - Tunnel ID
# List tunnels
cloudflared tunnel list
Step 2.3: Create Tunnel Configuration Directory
# Create config directory
mkdir -p ~/.cloudflared
# Note your tunnel ID (from the list command)
# It looks like: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Part 3: Configure Tunnel for Rancher
Step 3.1: Create Tunnel Configuration File
# Create config.yml
cat > ~/.cloudflared/config.yml << EOF
tunnel: k3s-cluster
credentials-file: /root/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
ingress:
# Rancher dashboard
- hostname: rancher.your-domain.com
service: https://localhost:30443
originRequest:
noTLSVerify: true
# Default backend
- service: http_status:404
EOF
Replace:
– xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx with your actual tunnel ID
– rancher.your-domain.com with your actual domain
Step 3.2: Route DNS to Tunnel
# Route your domain to the tunnel
cloudflared tunnel route dns k3s-cluster rancher.your-domain.com
# Verify DNS record
cloudflared tunnel list
Part 4: Deploy Tunnel in Kubernetes
Step 4.1: Create Kubernetes Secret for Tunnel Credentials
# Create namespace for cloudflare
kubectl create namespace cloudflare
# Create secret from credentials file
kubectl create secret generic tunnel-credentials \
--namespace cloudflare \
--from-file=credentials.json=/root/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
Step 4.2: Deploy cloudflared as a Deployment
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflare
spec:
replicas: 2
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --config
- /etc/cloudflared/config/config.yml
- run
volumeMounts:
- name: config
mountPath: /etc/cloudflared/config
readOnly: true
- name: creds
mountPath: /etc/cloudflared/creds
readOnly: true
env:
- name: TUNNEL_CRED_FILE
value: /etc/cloudflared/creds/credentials.json
volumes:
- name: config
configMap:
name: tunnel-config
- name: creds
secret:
secretName: tunnel-credentials
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tunnel-config
namespace: cloudflare
data:
config.yml: |
tunnel: k3s-cluster
credentials-file: /etc/cloudflared/creds/credentials.json
ingress:
- hostname: rancher.your-domain.com
service: https://rancher.cattle-system.svc.cluster.local:443
originRequest:
noTLSVerify: true
- service: http_status:404
EOF
Note: This uses Kubernetes service DNS (rancher.cattle-system.svc.cluster.local) instead of NodePort.
Step 4.3: Verify Tunnel Pods
# Check pods
kubectl -n cloudflare get pods
# Check logs
kubectl -n cloudflare logs -l app=cloudflared
# Should show "Connected to Cloudflare Edge"
Part 5: Expose Additional Services
Step 5.1: Add More Ingress Rules
Update the ConfigMap to add more services:
kubectl -n cloudflare edit configmap tunnel-config
Add more ingress rules:
ingress:
- hostname: rancher.your-domain.com
service: https://rancher.cattle-system.svc.cluster.local:443
originRequest:
noTLSVerify: true
- hostname: app1.your-domain.com
service: http://app1-service.default.svc.cluster.local:80
- hostname: app2.your-domain.com
service: http://app2-service.default.svc.cluster.local:8080
- hostname: grafana.your-domain.com
service: http://monitoring-grafana.monitoring.svc.cluster.local:80
- service: http_status:404
Step 5.2: Route DNS for Each Service
# Route each domain to the tunnel
cloudflared tunnel route dns k3s-cluster app1.your-domain.com
cloudflared tunnel route dns k3s-cluster app2.your-domain.com
cloudflared tunnel route dns k3s-cluster grafana.your-domain.com
Part 6: Add Zero Trust Security (Optional)
Step 6.1: Enable Cloudflare Access
- Go to Cloudflare Dashboard → Zero Trust → Access → Applications
- Add an application
- Select your domain (rancher.your-domain.com)
- Configure authentication (Google, GitHub, etc.)
- Set access policies
Step 6.2: Add Access Policy to Tunnel
Update your tunnel config to use Access:
kubectl -n cloudflare edit configmap tunnel-config
Add Access configuration:
ingress:
- hostname: rancher.your-domain.com
service: https://rancher.cattle-system.svc.cluster.local:443
originRequest:
noTLSVerify: true
accessTeamName: your-team-name # from Cloudflare Zero Trust
accessServiceToken: # optional for service-to-service
- service: http_status:404
Part 7: High Availability Setup
Step 7.1: Deploy on Multiple Nodes
# Add node affinity to spread pods
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflare
spec:
replicas: 3
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cloudflared
topologyKey: kubernetes.io/hostname
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --config
- /etc/cloudflared/config/config.yml
- run
volumeMounts:
- name: config
mountPath: /etc/cloudflared/config
- name: creds
mountPath: /etc/cloudflared/creds
volumes:
- name: config
configMap:
name: tunnel-config
- name: creds
secret:
secretName: tunnel-credentials
EOF
Step 7.2: Monitor Tunnel Health
# Check all replicas are running
kubectl -n cloudflare get pods -o wide
# Monitor logs
kubectl -n cloudflare logs -f -l app=cloudflared --max-log-requests=3
# Check tunnel status (from master)
cloudflared tunnel list
cloudflared tunnel info k3s-cluster
Part 8: Test Your Setup
Step 8.1: Deploy Test Application
# Deploy a simple web app
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-world
spec:
replicas: 2
selector:
matchLabels:
app: hello-world
template:
metadata:
labels:
app: hello-world
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello-world
spec:
selector:
app: hello-world
ports:
- port: 80
targetPort: 80
EOF
Step 8.2: Add to Tunnel
# Update tunnel config
kubectl -n cloudflare edit configmap tunnel-config
# Add:
# - hostname: hello.your-domain.com
# service: http://hello-world.default.svc.cluster.local:80
# Route DNS
cloudflared tunnel route dns k3s-cluster hello.your-domain.com
Step 8.3: Test Access
# From any browser
curl https://hello.your-domain.com
# Should show nginx welcome page
# Check tunnel metrics
cloudflared tunnel metrics
Part 9: Monitoring and Maintenance
Step 9.1: Monitor Tunnel Metrics
# Enable metrics endpoint
kubectl -n cloudflare edit deploy cloudflared
# Add args:
# - --metrics
# - 0.0.0.0:2000
# Create service for metrics
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: cloudflared-metrics
namespace: cloudflare
spec:
selector:
app: cloudflared
ports:
- port: 2000
targetPort: 2000
name: metrics
EOF
# Access metrics
kubectl -n cloudflare port-forward svc/cloudflared-metrics 2000:2000
# Open browser: http://localhost:2000/metrics
Step 9.2: Logging
# View tunnel logs
kubectl -n cloudflare logs -f -l app=cloudflared
# Check Cloudflare dashboard for analytics
# https://dash.cloudflare.com/ → Zero Trust → Networks → Tunnels
Step 9.3: Update cloudflared
# Update image version
kubectl -n cloudflare set image deploy/cloudflared cloudflared=cloudflare/cloudflared:latest
# Rollout status
kubectl -n cloudflare rollout status deploy/cloudflared
Part 10: Troubleshooting
Issue: Tunnel Not Connecting
# Check tunnel status
cloudflared tunnel list
cloudflared tunnel info k3s-cluster
# Check pod logs
kubectl -n cloudflare logs -l app=cloudflared --tail=50
# Verify credentials secret
kubectl -n cloudflare get secret tunnel-credentials -o yaml
# Test tunnel locally (on master)
cloudflared tunnel run k3s-cluster
Issue: DNS Not Resolving
# Check DNS routes
cloudflared tunnel route ip show
# Verify DNS records
dig rancher.your-domain.com
# Re-route if needed
cloudflared tunnel route dns k3s-cluster rancher.your-domain.com --force
Issue: Certificate Errors
# Update config to skip TLS verification for internal services
originRequest:
noTLSVerify: true
# Or use HTTP for internal services if secure
service: http://service-name.namespace.svc.cluster.local:port
Issue: Access Denied (Zero Trust)
# Check Access configuration in Cloudflare dashboard
# Verify team name in config
# Check service tokens if used
Part 11: Complete Working Example
Here’s a complete setup script for your cluster:
#!/bin/bash
# Complete Cloudflare Tunnel setup for K3s
# Variables - CHANGE THESE
DOMAIN="your-domain.com"
TUNNEL_NAME="k3s-cluster"
# 1. Install cloudflared
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64
mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
# 2. Login (manual step - will open browser)
echo "Run: cloudflared tunnel login"
echo "Then press Enter to continue"
read
# 3. Create tunnel
cloudflared tunnel create $TUNNEL_NAME
TUNNEL_ID=$(cloudflared tunnel list | grep $TUNNEL_NAME | awk '{print $1}')
# 4. Create namespace
kubectl create namespace cloudflare
# 5. Create secret
kubectl create secret generic tunnel-credentials \
--namespace cloudflare \
--from-file=credentials.json=/root/.cloudflared/$TUNNEL_ID.json
# 6. Create configmap
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: tunnel-config
namespace: cloudflare
data:
config.yml: |
tunnel: $TUNNEL_NAME
credentials-file: /etc/cloudflared/creds/credentials.json
ingress:
- hostname: rancher.$DOMAIN
service: https://rancher.cattle-system.svc.cluster.local:443
originRequest:
noTLSVerify: true
- service: http_status:404
EOF
# 7. Deploy cloudflared
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflare
spec:
replicas: 2
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --config
- /etc/cloudflared/config/config.yml
- run
volumeMounts:
- name: config
mountPath: /etc/cloudflared/config
- name: creds
mountPath: /etc/cloudflared/creds
volumes:
- name: config
configMap:
name: tunnel-config
- name: creds
secret:
secretName: tunnel-credentials
EOF
# 8. Route DNS
cloudflared tunnel route dns $TUNNEL_NAME rancher.$DOMAIN
# 9. Wait for deployment
kubectl -n cloudflare rollout status deploy/cloudflared
# 10. Show status
echo "================================="
echo "Tunnel setup complete!"
echo "Access Rancher at: https://rancher.$DOMAIN"
echo "Username: admin"
echo "Password: admin123"
echo "================================="
Security Best Practices
- Use Zero Trust Access for authentication
- Enable audit logs in Cloudflare
- Use service tokens for automated access
- Regularly rotate credentials
- Monitor tunnel metrics for anomalies
- Use separate tunnels for different environments
- Enable WAF rules in Cloudflare
Cost Considerations
- Cloudflare Tunnel: Free
- Cloudflare Access: Free for up to 50 users
- DNS management: Free
- WAF/security: Paid features available
Benefits of This Setup
✅ No open firewall ports – maximum security
✅ DDoS protection – Cloudflare edge
✅ SSL/TLS termination – automatic certificates
✅ Load balancing – multiple tunnel replicas
✅ Zero Trust ready – add SSO authentication
✅ Global CDN – cached content at edge
✅ Analytics – detailed traffic insights
Your services are now securely exposed to the internet through Cloudflare’s global network, with no inbound ports needed on your Proxmox host or VMs!