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

  1. Sign up/Login at dash.cloudflare.com
  2. Add your domain (or use a free domain you own)
  3. 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

  1. Go to Cloudflare Dashboard → Zero Trust → Access → Applications
  2. Add an application
  3. Select your domain (rancher.your-domain.com)
  4. Configure authentication (Google, GitHub, etc.)
  5. 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

  1. Use Zero Trust Access for authentication
  2. Enable audit logs in Cloudflare
  3. Use service tokens for automated access
  4. Regularly rotate credentials
  5. Monitor tunnel metrics for anomalies
  6. Use separate tunnels for different environments
  7. 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!