Cloudflared Cheat Sheet
Overview
Cloudflared is the open-source daemon that powers Cloudflare Tunnel (formerly Argo Tunnel). It creates persistent, encrypted tunnels from your servers or local machines to Cloudflare’s global edge network, allowing you to expose services publicly without opening any inbound firewall ports or configuring NAT rules. The tunnel works by establishing outbound WebSocket connections from your server to Cloudflare’s nearest edge PoP, and all incoming traffic is proxied inward through this connection.
The security model is fundamentally different from traditional reverse proxies or VPNs. With cloudflared, your origin server never needs a public IP address or exposed ports — the connection is always initiated outbound from your infrastructure. All traffic passes through Cloudflare’s network, providing DDoS protection, WAF capabilities, caching, and Cloudflare Access (zero-trust identity-aware proxy) without additional configuration on your end. Cloudflare Access can gate any tunneled application behind Google, GitHub, Azure AD, or any OIDC/SAML identity provider, requiring authentication before any request reaches your origin.
Beyond HTTP/HTTPS tunneling, cloudflared supports TCP and UDP tunneling via cloudflare tunnel route tcp, private network routing (making entire subnets accessible to WARP-connected users), and SSH/RDP proxying. It integrates directly with systemd, Docker, Kubernetes, and can be deployed as a Kubernetes DaemonSet or deployment for highly available tunnel endpoints.
Installation
Linux (Official Package)
# Debian/Ubuntu — add Cloudflare repo
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared jammy main' \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
# RHEL/CentOS/Fedora
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.repo \
| sudo tee /etc/yum.repos.d/cloudflare-main.repo
sudo dnf install -y cloudflared
# Verify installation
cloudflared --version
macOS
# Homebrew
brew install cloudflare/cloudflare/cloudflared
# Or download binary directly
curl -OL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz
tar -xzf cloudflared-darwin-amd64.tgz
sudo mv cloudflared /usr/local/bin/
cloudflared --version
Windows
# winget
winget install Cloudflare.cloudflared
# Chocolatey
choco install cloudflared
# Or download MSI from GitHub releases
# https://github.com/cloudflare/cloudflared/releases/latest
Docker
# Run cloudflared tunnel in Docker
docker run -d \
--name cloudflared \
--restart unless-stopped \
cloudflare/cloudflared:latest tunnel \
--no-autoupdate run --token YOUR_TUNNEL_TOKEN
# Docker Compose
cat > docker-compose.yml << 'EOF'
version: "3.9"
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
networks:
- app_network
myapp:
image: nginx:latest
container_name: myapp
networks:
- app_network
networks:
app_network:
EOF
Kubernetes
# Create secret with tunnel token
kubectl create secret generic cloudflared-secret \
--from-literal=TUNNEL_TOKEN=YOUR_TUNNEL_TOKEN \
-n cloudflared
# Deploy as Deployment
cat > cloudflared-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflared
spec:
replicas: 2 # Run 2 connectors for HA
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --no-autoupdate
- run
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: cloudflared-secret
key: TUNNEL_TOKEN
EOF
kubectl apply -f cloudflared-deployment.yaml
Configuration
Authenticate cloudflared
# Log in to Cloudflare (opens browser)
cloudflared tunnel login
# This stores cert.pem in ~/.cloudflared/
ls ~/.cloudflared/
# cert.pem — your account credentials
Tunnel Configuration File (~/.cloudflared/config.yml)
# Tunnel UUID or name
tunnel: my-tunnel-name
# or: tunnel: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Path to tunnel credentials file
credentials-file: /home/user/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
# Logging
logfile: /var/log/cloudflared.log
loglevel: info
# Ingress rules — processed top to bottom
ingress:
# Route app.example.com to local port 3000
- hostname: app.example.com
service: http://localhost:3000
# Route API traffic to different port
- hostname: api.example.com
service: http://localhost:8080
originRequest:
connectTimeout: 30s
tlsTimeout: 30s
tcpKeepAlive: 30s
# SSH proxy (browser-based SSH via Cloudflare Access)
- hostname: ssh.example.com
service: ssh://localhost:22
# HTTPS with custom origin cert verification
- hostname: secure.example.com
service: https://localhost:8443
originRequest:
noTLSVerify: true # Skip origin cert verification
# Catch-all — required as the last rule
- service: http_status:404
Origin Request Options
originRequest:
connectTimeout: 30s # Timeout for TCP connection to origin
tlsTimeout: 10s # Timeout for TLS handshake
tcpKeepAlive: 30s # TCP keepalive interval
keepAliveConnections: 100 # Max idle keepalive connections
keepAliveTimeout: 90s # How long to keep idle connections alive
httpHostHeader: "" # Override Host header sent to origin
noTLSVerify: false # Skip origin TLS cert verification
disableChunkedEncoding: false
proxyAddress: "127.0.0.1" # Proxy bind address
proxyPort: 0 # Proxy port (0 = auto)
Core Commands
| Command | Description |
|---|---|
cloudflared tunnel login | Authenticate to Cloudflare account |
cloudflared tunnel create NAME | Create a new named tunnel |
cloudflared tunnel list | List all tunnels in account |
cloudflared tunnel info NAME | Show tunnel details and connections |
cloudflared tunnel run NAME | Run a tunnel (using config.yml) |
cloudflared tunnel run --token TOKEN | Run tunnel using a token (no config file) |
cloudflared tunnel delete NAME | Delete a tunnel |
cloudflared tunnel cleanup NAME | Remove stale tunnel connections |
cloudflared tunnel route dns NAME HOSTNAME | Create DNS CNAME record for tunnel |
cloudflared tunnel route ip add CIDR NAME | Route CIDR through tunnel (private network) |
cloudflared tunnel route ip show | List IP routes through tunnels |
cloudflared tunnel ingress validate | Validate ingress rules in config |
cloudflared tunnel ingress rule HOSTNAME | Test which ingress rule matches hostname |
cloudflared service install | Install as system service (systemd/launchd/Windows) |
cloudflared service uninstall | Remove system service |
cloudflared proxy-dns | Run as DNS-over-HTTPS proxy |
cloudflared access tcp --hostname HOST --url local:PORT | Open TCP tunnel (SSH proxy) |
cloudflared access ssh --hostname HOST | Open SSH session through Access |
cloudflared version | Show version information |
cloudflared help | Show help |
Advanced Usage
Creating and Running a Tunnel
# 1. Create the tunnel
cloudflared tunnel create my-app
# Output: Created tunnel my-app with id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Credentials file stored at: ~/.cloudflared/xxxxxxxx...json
# 2. Create config.yml
cat > ~/.cloudflared/config.yml << 'EOF'
tunnel: my-app
credentials-file: /home/user/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
ingress:
- hostname: app.example.com
service: http://localhost:3000
- service: http_status:404
EOF
# 3. Create DNS record
cloudflared tunnel route dns my-app app.example.com
# 4. Run the tunnel
cloudflared tunnel run my-app
# 5. Or install as a system service
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Remote-Managed Tunnels (Recommended)
# Create tunnel via Cloudflare Dashboard
# Zero Trust > Networks > Tunnels > Create a tunnel
# Copy the tunnel token
# Run with token — no local config file needed
cloudflared tunnel run --token eyJhIjoixxxxxx...
# Install as service with token
cloudflared service install --token eyJhIjoixxxxxx...
sudo systemctl enable --now cloudflared
# In Docker
docker run -d \
--name cloudflared \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate run --token eyJhIjoixxxxxx...
Private Network Routing (WARP to Tunnel)
# Route a private CIDR through your tunnel
# Users must connect via Cloudflare WARP client
# Add IP route to tunnel
cloudflared tunnel route ip add 10.0.0.0/8 my-tunnel
# Or specific subnet
cloudflared tunnel route ip add 192.168.1.0/24 my-tunnel
# Show all routes
cloudflared tunnel route ip show
# Remove a route
cloudflared tunnel route ip delete 192.168.1.0/24
# Users install WARP client and log in to your Zero Trust organization
# Then access 192.168.1.x directly without a VPN config
SSH Access via Cloudflare Access
# In config.yml — expose SSH
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
- service: http_status:404
# On the SSH server — configure the tunnel
cloudflared tunnel route dns my-tunnel ssh.example.com
# In Cloudflare Zero Trust dashboard:
# Access > Applications > Add Application > Self-hosted
# Configure identity provider policy for ssh.example.com
# On client machine — install cloudflared
# Add to ~/.ssh/config:
cat >> ~/.ssh/config << 'EOF'
Host ssh.example.com
ProxyCommand cloudflared access ssh --hostname %h
User ubuntu
EOF
# Now SSH as normal
ssh ssh.example.com
TCP Tunnel for Non-HTTP Services
# Expose a TCP service (e.g., RDP, database)
# In config.yml:
ingress:
- hostname: rdp.example.com
service: tcp://localhost:3389
- service: http_status:404
# On client — create local proxy for TCP connection
cloudflared access tcp --hostname rdp.example.com --url localhost:3389
# Now connect your RDP client to localhost:3389
# Cloudflare Access gates the connection
Kubernetes Ingress with Cloudflare Tunnel
# cloudflared-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cloudflared-config
namespace: cloudflared
data:
config.yaml: |
tunnel: my-k8s-tunnel
credentials-file: /etc/cloudflared/creds/credentials.json
ingress:
- hostname: app.example.com
service: http://nginx-service.default.svc.cluster.local:80
- hostname: api.example.com
service: http://api-service.default.svc.cluster.local:8080
- service: http_status:404
# Create credentials secret
kubectl create secret generic tunnel-credentials \
--from-file=credentials.json=$HOME/.cloudflared/TUNNEL_UUID.json \
-n cloudflared
High Availability Setup
# Run multiple cloudflared instances for HA
# Cloudflare automatically load balances across connectors
# On server 1
cloudflared tunnel run my-tunnel # Establishes 2 connections
# On server 2 (same tunnel, same credentials)
cloudflared tunnel run my-tunnel # Establishes 2 more connections
# Tunnel info shows all active connections
cloudflared tunnel info my-tunnel
# 4 connections from 2 servers = HA with automatic failover
# In Kubernetes: set replicas: 2 or 3 in Deployment
Monitoring and Diagnostics
# View tunnel status
cloudflared tunnel info my-tunnel
# Check active connections in Cloudflare dashboard
# Zero Trust > Networks > Tunnels > [tunnel name] > Overview
# Enable debug logging
cloudflared tunnel run --loglevel debug my-tunnel
# Check metrics endpoint (enabled by default on :2000)
curl http://localhost:2000/metrics
# Health check endpoint
curl http://localhost:2000/healthcheck
# View logs (systemd)
journalctl -u cloudflared -f
# View logs (Docker)
docker logs -f cloudflared
Common Workflows
Exposing a Local Development Server
# Quick tunnel without config file (temporary)
cloudflared tunnel --url http://localhost:3000
# Output:
# Your quick Tunnel has been created! Visit it at:
# https://random-name.trycloudflare.com
# No account required for quick tunnels!
# Great for demos, webhooks, and testing
# Named quick tunnel (requires login)
cloudflared tunnel --url http://localhost:8080 \
--hostname dev.example.com
Migrating from Port Forwarding to Cloudflare Tunnel
# Before: exposed via router port forward + dynamic DNS
# Security risk: open ports, DDoS exposure
# After: close all inbound ports
sudo ufw delete allow 80/tcp
sudo ufw delete allow 443/tcp
sudo ufw delete allow 22/tcp
# Create tunnel
cloudflared tunnel create homeserver
cat > /etc/cloudflared/config.yml << 'EOF'
tunnel: homeserver
credentials-file: /root/.cloudflared/UUID.json
ingress:
- hostname: home.example.com
service: http://localhost:80
- hostname: nas.example.com
service: http://localhost:5000
- hostname: ssh.example.com
service: ssh://localhost:22
- service: http_status:404
EOF
cloudflared tunnel route dns homeserver home.example.com
cloudflared tunnel route dns homeserver nas.example.com
cloudflared tunnel route dns homeserver ssh.example.com
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Zero-Trust Access Policy Setup
# After exposing service via tunnel, configure Access policy
# in Cloudflare Zero Trust dashboard:
# 1. Access > Applications > Add Application
# 2. Choose "Self-hosted"
# 3. Set hostname: app.example.com
# 4. Configure identity provider (Google, GitHub, OKTA, etc.)
# 5. Define policy: require email matches @example.com
# Now all access to app.example.com requires authentication
# No username/password on the application itself needed
Tips and Best Practices
| Practice | Details |
|---|---|
| Use remote-managed tunnels | Dashboard-managed tunnels are easier to configure and update without SSH |
| Run 2+ connectors for HA | Deploy cloudflared on multiple hosts for automatic failover |
| Token-based auth for servers | Use --token flag in production — no need to copy credential files |
| Restrict origin to localhost | Bind your services to 127.0.0.1 only; tunnel handles all external access |
| Use Cloudflare Access | Layer zero-trust identity policies over every tunneled application |
| Monitor connector health | Check /healthcheck endpoint or Cloudflare dashboard for connector status |
Set noTLSVerify carefully | Only disable origin TLS verification for internal services you control |
| Private network over split tunnel | Use tunnel route ip for network-wide access instead of many individual tunnels |
| Pin cloudflared version | Use cloudflare/cloudflared:2024.x.x instead of :latest in production |
Use --no-autoupdate | In containers, disable auto-update; control versions via image tags |
| DNS propagation | After tunnel route dns, wait up to 60 seconds for DNS to propagate |
| Cleanup stale connections | Run cloudflared tunnel cleanup NAME after ungraceful shutdowns |