Cloudflare Tunnel Setup Guide for Raspberry Pi
Target Setup for hotspizza.org: -
admin.hotspizza.org→ Web portal (localhost:5000) -ssh.hotspizza.org→ Browser-based SSH (localhost:22)
Table of Contents
- Overview
- Prerequisites
- Installing cloudflared
- Authentication
- Creating the Tunnel
- Configuration
- DNS Routing
- Running as a Service
- Browser-Based SSH
- Testing
- Troubleshooting
Overview
What is Cloudflare Tunnel?
Cloudflare Tunnel (formerly Argo Tunnel) creates a secure outbound-only connection from your Raspberry Pi to Cloudflare's edge network. This allows you to expose local services without: - Port forwarding - Exposing your home IP address - Complex firewall rules
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ INTERNET │
│ │
│ Browser ──────► admin.hotspizza.org ──────► Cloudflare Edge │
│ ssh.hotspizza.org │
└───────────────────────────────┬─────────────────────────────────────┘
│
│ Encrypted Tunnel (outbound from Pi)
│
┌───────────────────────────────▼─────────────────────────────────────┐
│ RASPBERRY PI │
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ cloudflared │───►│ localhost:5000 │ │ localhost:22 │ │
│ │ daemon │ │ (uvicorn/FastAPI)│ │ (SSH server) │ │
│ └─────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Two Tunnel Management Approaches
| Approach | Pros | Cons |
|---|---|---|
| Locally-Managed (CLI) | Full control, config in version control, works offline | More manual setup |
| Remotely-Managed (Dashboard) | Easy UI, no config files | Requires dashboard access to modify |
This guide uses locally-managed tunnels for maximum control and reproducibility.
Prerequisites
Before starting, ensure you have:
- Cloudflare Account (free tier is sufficient)
- Domain added to Cloudflare with nameservers pointed to Cloudflare
- In our case:
hotspizza.org - Raspberry Pi with:
- 64-bit OS (check with
uname -m→ should showaarch64) - Internet connectivity
- SSH access
Verify Your Pi Architecture
uname -m
# Expected output: aarch64 (64-bit ARM)
Note: Pi Zero W (original), Model 1B, and 2B may have issues with cloudflared. Pi Zero 2 W with 64-bit OS works fine.
Installing cloudflared
Method 1: APT Repository (Recommended)
This method provides automatic updates:
# Add Cloudflare's GPG key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
# Add the repository
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# Install
sudo apt update && sudo apt install cloudflared -y
# Verify
cloudflared --version
Method 2: Direct .deb Package
If the APT method fails:
# Download the ARM64 .deb package
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb -o /tmp/cloudflared.deb
# Install
sudo dpkg -i /tmp/cloudflared.deb
# Verify
cloudflared --version
Method 3: Direct Binary
Fallback if .deb has issues:
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -o /tmp/cloudflared
chmod +x /tmp/cloudflared
sudo mv /tmp/cloudflared /usr/local/bin/cloudflared
cloudflared --version
Authentication
Log in to Cloudflare to authorize tunnel creation:
cloudflared tunnel login
This will:
1. Print a URL to the terminal
2. Open that URL in a browser (or copy/paste it)
3. Ask you to select which domain to authorize
4. Generate ~/.cloudflared/cert.pem (account certificate)
Select your domain (e.g., hotspizza.org) when prompted.
Verify Authentication
ls -la ~/.cloudflared/
# Should show: cert.pem
Creating the Tunnel
Create a named tunnel:
cloudflared tunnel create payphone
This outputs:
Tunnel credentials written to /home/pi/.cloudflared/<TUNNEL-UUID>.json
Created tunnel payphone with id <TUNNEL-UUID>
Save the UUID - you'll need it for the config file.
List Tunnels
cloudflared tunnel list
(Optional) Rename Credentials File
For clarity, rename the credentials file:
mv ~/.cloudflared/<TUNNEL-UUID>.json ~/.cloudflared/payphone.json
Configuration
Create the tunnel configuration file:
nano ~/.cloudflared/config.yml
Basic Config (Web Portal Only)
tunnel: payphone
credentials-file: /home/hots/.cloudflared/payphone.json
ingress:
- hostname: admin.hotspizza.org
service: http://localhost:5000
- service: http_status:404
Full Config (Web Portal + SSH)
tunnel: payphone
credentials-file: /home/hots/.cloudflared/payphone.json
ingress:
# Web portal
- hostname: admin.hotspizza.org
service: http://localhost:5000
# SSH (browser-rendered)
- hostname: ssh.hotspizza.org
service: ssh://localhost:22
# Catch-all (required)
- service: http_status:404
Configuration Options Explained
| Field | Description |
|---|---|
tunnel |
Tunnel name or UUID |
credentials-file |
Path to the JSON credentials file |
ingress |
List of hostname → service mappings |
service: http_status:404 |
Catch-all for unmatched requests (required) |
Validate Config
cloudflared tunnel ingress validate
DNS Routing
Create CNAME records that point your subdomains to the tunnel:
# Web portal
cloudflared tunnel route dns payphone admin.hotspizza.org
# SSH (if using)
cloudflared tunnel route dns payphone ssh.hotspizza.org
Each command creates a CNAME record in Cloudflare DNS pointing to <TUNNEL-UUID>.cfargotunnel.com.
Verify DNS Records
Check in Cloudflare Dashboard → DNS → Records, or:
cloudflared tunnel route ip show
Running as a Service
Install as System Service
# Copy config to system location
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/config.yml
sudo cp ~/.cloudflared/payphone.json /etc/cloudflared/payphone.json
# Update paths in config (change /home/hots to /etc/cloudflared)
sudo sed -i 's|/home/hots/.cloudflared|/etc/cloudflared|g' /etc/cloudflared/config.yml
# Install the service
sudo cloudflared service install
# Enable and start
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
# Check status
sudo systemctl status cloudflared
View Tunnel Logs
sudo journalctl -u cloudflared -f
Browser-Based SSH
Cloudflare can render an SSH terminal directly in the browser. This is convenient because: - No SSH client needed on the connecting device - Works from any browser (phone, tablet, etc.) - Can be protected with Cloudflare Access policies
How It Works
When you visit https://ssh.hotspizza.org:
1. Cloudflare authenticates you (optional Access policy)
2. Renders a terminal in the browser
3. Proxies SSH traffic through the tunnel
Setup Steps
- Tunnel config (already done above): ```yaml
-
hostname: ssh.hotspizza.org service: ssh://localhost:22 ```
-
DNS route (already done above):
bash cloudflared tunnel route dns payphone ssh.hotspizza.org -
(Recommended) Add Access Policy:
- Go to Cloudflare Zero Trust Dashboard
- Applications → Add Application → Self-hosted
- Set domain:
ssh.hotspizza.org - Add authentication (email OTP, Google, etc.)
Native SSH (Alternative)
If you prefer using a native SSH client, you need cloudflared on your local machine too:
On your local machine:
# Install cloudflared locally
brew install cloudflared # macOS
# or download from GitHub for your OS
# Add to ~/.ssh/config
Host ssh.hotspizza.org
ProxyCommand cloudflared access ssh --hostname %h
Then SSH normally:
ssh user@ssh.hotspizza.org
Testing
Test Tunnel Locally
Before running as a service, test manually:
cloudflared tunnel run payphone
Leave this running and test in another terminal or browser.
Test Web Portal
curl https://admin.hotspizza.org
# Should return your web portal HTML
Or open in browser: https://admin.hotspizza.org
Test SSH (Browser)
Open in browser: https://ssh.hotspizza.org
Should show a terminal login prompt.
Check Tunnel Info
cloudflared tunnel info payphone
Troubleshooting
Common Issues
1. "failed to connect to origin"
Cause: Local service not running
Fix: Ensure uvicorn/FastAPI is running on port 5000:
systemctl --user status payphone-web
2. "Unable to reach the origin service"
Cause: Wrong port or service not listening
Fix: Verify the service is listening:
ss -tlnp | grep 5000
3. DNS not resolving
Cause: CNAME not created or propagating
Fix: Check Cloudflare DNS dashboard, wait a few minutes, or:
dig admin.hotspizza.org CNAME
4. "Tunnel credentials file not found"
Cause: Wrong path in config.yml
Fix: Verify the credentials file path matches your config:
ls -la ~/.cloudflared/
cat ~/.cloudflared/config.yml
5. Certificate errors
Cause: cert.pem missing or expired
Fix: Re-authenticate:
cloudflared tunnel login
Useful Debug Commands
# Check tunnel status
cloudflared tunnel info payphone
# List all tunnels
cloudflared tunnel list
# Validate config
cloudflared tunnel ingress validate
# Test connectivity
cloudflared tunnel run --loglevel debug payphone
# Check systemd service
sudo systemctl status cloudflared
sudo journalctl -u cloudflared --since "10 minutes ago"
Port 7844 Required
Cloudflared needs outbound access to port 7844 (QUIC). If behind a restrictive firewall:
# Test connectivity
cloudflared tunnel connectivity
Quick Reference Commands
# Install
sudo apt install cloudflared
# Authenticate
cloudflared tunnel login
# Create tunnel
cloudflared tunnel create <NAME>
# Add DNS route
cloudflared tunnel route dns <NAME> <HOSTNAME>
# Run manually
cloudflared tunnel run <NAME>
# Install as service
sudo cloudflared service install
# Service management
sudo systemctl start cloudflared
sudo systemctl stop cloudflared
sudo systemctl restart cloudflared
sudo systemctl status cloudflared
# View logs
sudo journalctl -u cloudflared -f
# Tunnel info
cloudflared tunnel info <NAME>
cloudflared tunnel list
References
- Cloudflare Tunnel Documentation
- Create a locally-managed tunnel
- SSH Browser Rendering
- Pi My Life Up - Raspberry Pi Cloudflare Tunnel
- cloudflared Downloads
Appendix: Full Setup Script
Here's a complete script for reference (adjust paths as needed):
#!/bin/bash
set -e
TUNNEL_NAME="payphone"
DOMAIN="hotspizza.org"
WEB_HOSTNAME="admin.$DOMAIN"
SSH_HOSTNAME="ssh.$DOMAIN"
CREDS_FILE="/etc/cloudflared/${TUNNEL_NAME}.json"
CONFIG_FILE="/etc/cloudflared/config.yml"
echo "=== Installing cloudflared ==="
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared -y
echo "=== Authenticating ==="
cloudflared tunnel login
echo "=== Creating tunnel ==="
cloudflared tunnel create $TUNNEL_NAME
# Get tunnel UUID
TUNNEL_UUID=$(cloudflared tunnel list | grep $TUNNEL_NAME | awk '{print $1}')
echo "=== Setting up config ==="
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/${TUNNEL_UUID}.json $CREDS_FILE
sudo tee $CONFIG_FILE > /dev/null << EOF
tunnel: $TUNNEL_NAME
credentials-file: $CREDS_FILE
ingress:
- hostname: $WEB_HOSTNAME
service: http://localhost:5000
- hostname: $SSH_HOSTNAME
service: ssh://localhost:22
- service: http_status:404
EOF
echo "=== Creating DNS routes ==="
cloudflared tunnel route dns $TUNNEL_NAME $WEB_HOSTNAME
cloudflared tunnel route dns $TUNNEL_NAME $SSH_HOSTNAME
echo "=== Installing service ==="
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
echo "=== Done! ==="
echo "Web portal: https://$WEB_HOSTNAME"
echo "SSH: https://$SSH_HOSTNAME"
sudo systemctl status cloudflared