DEV Community

Cover image for Cloudflare Tunnel SSH Setup Guide
Bipin C
Bipin C

Posted on

Cloudflare Tunnel SSH Setup Guide

Complete Terminal-Based Remote Access with Docker

This guide enables SSH access to your server from anywhere using your terminal, with Cloudflare Tunnel running in Docker.

What you'll achieve: Run ssh myserver from any network and connect to your home/office server securely.


Architecture Overview

Security Benefits:

  • No ports exposed on your router
  • All traffic encrypted through Cloudflare
  • Server IP hidden from the internet
  • SSH key authentication (no passwords)
  • Cloudflare Access authentication layer

Prerequisites

  • Cloudflare account (free tier works)
  • Domain name with DNS managed by Cloudflare
  • Server: Ubuntu with Docker installed
  • Client: macOS, Linux, or Windows machine

PART 1: SERVER SETUP

Run all commands in this section on your server.


Step 1: Install SSH Server

Ubuntu may not have SSH server installed by default.

# Update package list
sudo apt update

# Install OpenSSH server
sudo apt install openssh-server -y

# Start and enable it
sudo systemctl start ssh
sudo systemctl enable ssh

# Verify it's running
sudo systemctl status ssh
Enter fullscreen mode Exit fullscreen mode

You should see: Active: active (running)

Verify it's listening on port 22:

sudo ss -tlnp | grep :22
Enter fullscreen mode Exit fullscreen mode

Expected output:

LISTEN 0  128  0.0.0.0:22  0.0.0.0:*  users:(("sshd",pid=xxxx,fd=3))
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Tunnel in Cloudflare Dashboard

  1. Open Cloudflare Zero Trust Dashboard
  2. Go to Networks → Tunnels
  3. Click Create a tunnel
  4. Select Cloudflared as connector type
  5. Name your tunnel (e.g., home-server)
  6. Select Docker as your environment
  7. Cloudflare shows a command like:
   docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token eyJhIjoiNWFi...
Enter fullscreen mode Exit fullscreen mode
  1. Copy the token (the long string after --token)

Step 3: Create Docker Compose Configuration

Create a directory for your tunnel:

mkdir -p ~/cloudflare-tunnel
cd ~/cloudflare-tunnel
Enter fullscreen mode Exit fullscreen mode

Create the environment file:

nano .env
Enter fullscreen mode Exit fullscreen mode

Add your token:

TUNNEL_TOKEN=eyJhIjoiNWFi...paste-your-full-token-here...
Enter fullscreen mode Exit fullscreen mode

Save (Ctrl+O, Enter) and exit (Ctrl+X).

Secure the file:

chmod 600 .env
Enter fullscreen mode Exit fullscreen mode

Create the Docker Compose file:

nano docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Add this configuration:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
    network_mode: host
Enter fullscreen mode Exit fullscreen mode

Save and exit.

Important: network_mode: host is required so the container can access the host's SSH on port 22.


Step 4: Start the Tunnel

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Verify it's running:

docker compose ps
Enter fullscreen mode Exit fullscreen mode

Expected output:

NAME          IMAGE                          STATUS
cloudflared   cloudflare/cloudflared:latest  Up X minutes
Enter fullscreen mode Exit fullscreen mode

Check logs to confirm connection:

docker compose logs -f cloudflared
Enter fullscreen mode Exit fullscreen mode

Look for: Registered tunnel connection or Connection registered

Press Ctrl+C to exit logs.


Step 5: Add SSH Public Hostname

Back in Cloudflare Dashboard:

  1. Go to Networks → Tunnels
  2. Click on your tunnel name (should show "HEALTHY")
  3. Click Public Hostname tab
  4. Click Add a public hostname
  5. Configure:
Field Value
Subdomain ssh (or any name you prefer)
Domain Select your domain from dropdown
Type SSH
URL localhost:22
  1. Click Save hostname

Your SSH endpoint is now: ssh.yourdomain.com


Step 6: Create Access Application

This adds authentication before anyone can connect.

In Cloudflare Zero Trust Dashboard:

  1. Go to Access → Applications
  2. Click Add an application
  3. Select Self-hosted
  4. Configure:
Field Value
Application name SSH Access
Session duration 24 hours
Application domain ssh.yourdomain.com
  1. Click Next

Add Access Policy:

  1. Policy name: Allow SSH
  2. Action: Allow
  3. Add a rule:
    • Selector: Emails
    • Value: your.email@example.com
  4. Click Next
  5. Leave advanced settings as default
  6. Click Add application

PART 2: CLIENT SETUP

Run all commands in this section on your client machine.


Step 7: Install SSH Client

macOS

SSH client is pre-installed. Verify:

ssh -V
Enter fullscreen mode Exit fullscreen mode

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install openssh-client -y

# Verify
ssh -V
Enter fullscreen mode Exit fullscreen mode

Windows

Option 1: Via Settings

  • Settings → Apps → Optional Features → Add a feature → OpenSSH Client → Install

Option 2: PowerShell (Admin)

Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
Enter fullscreen mode Exit fullscreen mode

Verify:

ssh -V
Enter fullscreen mode Exit fullscreen mode

Step 8: Install cloudflared

macOS

brew install cloudflared
Enter fullscreen mode Exit fullscreen mode

Linux (Ubuntu/Debian)

curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
Enter fullscreen mode Exit fullscreen mode

Windows

winget install --id Cloudflare.cloudflared
Enter fullscreen mode Exit fullscreen mode

Or download from: https://github.com/cloudflare/cloudflared/releases/latest

Verify installation:

cloudflared --version
Enter fullscreen mode Exit fullscreen mode

Step 9: Generate SSH Key

ssh-keygen -t ed25519 -C "your-email@example.com"
Enter fullscreen mode Exit fullscreen mode

When prompted:

  • File location: Press Enter to accept default (~/.ssh/id_ed25519)
  • Passphrase: Enter a password (recommended) or leave empty

This creates:

  • ~/.ssh/id_ed25519 — Private key (keep secret, never share)
  • ~/.ssh/id_ed25519.pub — Public key (goes to server)

Step 10: Configure SSH

Create/edit your SSH config:

macOS / Linux:

mkdir -p ~/.ssh
nano ~/.ssh/config
Enter fullscreen mode Exit fullscreen mode

Windows (PowerShell):

mkdir -Force $env:USERPROFILE\.ssh
notepad $env:USERPROFILE\.ssh\config
Enter fullscreen mode Exit fullscreen mode

Add this configuration:

Host myserver
    HostName ssh.yourdomain.com
    User your-linux-username
    ProxyCommand cloudflared access ssh --hostname %h
    IdentityFile ~/.ssh/id_ed25519
Enter fullscreen mode Exit fullscreen mode

Replace:

  • myserver → Any alias you want (e.g., homelab, workserver)
  • ssh.yourdomain.com → Your subdomain from Step 5
  • your-linux-username → Your username on the server

Save and exit.


Step 11: Copy Public Key to Server

First, connect to server with password:

ssh myserver
Enter fullscreen mode Exit fullscreen mode

You'll go through:

  1. Cloudflare authentication (browser opens, enter email, get code)
  2. Linux password prompt

Once connected on the server, set up the authorized_keys:

# Create .ssh directory
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# Open authorized_keys
nano ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

On your client (new terminal window), display your public key:

cat ~/.ssh/id_ed25519.pub
Enter fullscreen mode Exit fullscreen mode

Copy the entire output (starts with ssh-ed25519 ...).

Paste it into the server's authorized_keys file, save and exit.

Set correct permissions:

chmod 600 ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Exit the server:

exit
Enter fullscreen mode Exit fullscreen mode

Step 12: Test Key Authentication

ssh myserver
Enter fullscreen mode Exit fullscreen mode

This time it should:

  1. Go through Cloudflare authentication (if session expired)
  2. Connect directly using your SSH key — no password prompt!

Step 13: (Optional) Disable Password Authentication

Once key authentication works, disable passwords for better security.

On SERVER:

sudo nano /etc/ssh/sshd_config
Enter fullscreen mode Exit fullscreen mode

Find and change (or add):

PasswordAuthentication no
PubkeyAuthentication yes
Enter fullscreen mode Exit fullscreen mode

Restart SSH:

sudo systemctl restart ssh
Enter fullscreen mode Exit fullscreen mode

Warning: Make sure key auth works before doing this, or you could lock yourself out!


Quick Reference

Server Commands

Task Command
Check tunnel status docker compose ps
View tunnel logs docker compose logs -f cloudflared
Restart tunnel docker compose restart cloudflared
Stop tunnel docker compose down
Start tunnel docker compose up -d
Update cloudflared docker compose pull && docker compose up -d
Check SSH status sudo systemctl status ssh
Restart SSH sudo systemctl restart ssh

Client Commands

Task Command
Connect to server ssh myserver
Connect with verbose ssh -v myserver
Clear Cloudflare auth cloudflared access logout
View public key cat ~/.ssh/id_ed25519.pub

File Structure Summary

Server

~/cloudflare-tunnel/
├── docker-compose.yml
└── .env                    # chmod 600

~/.ssh/
└── authorized_keys         # chmod 600 (contains client's public key)
Enter fullscreen mode Exit fullscreen mode

Client

~/.ssh/
├── config                  # SSH configuration
├── id_ed25519              # Private key (never share!)
└── id_ed25519.pub          # Public key (copied to server)
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Tunnel shows "INACTIVE" in Cloudflare dashboard

# On SERVER
docker compose ps
docker compose logs cloudflared
docker compose restart cloudflared
Enter fullscreen mode Exit fullscreen mode

"Connection refused"

# On SERVER - Check SSH is running
sudo systemctl status ssh

# Check cloudflared is using host network
docker inspect cloudflared | grep NetworkMode
# Should show: "NetworkMode": "host"
Enter fullscreen mode Exit fullscreen mode

"Permission denied" after setting up keys

# On SERVER - Check permissions
ls -la ~/.ssh/
# authorized_keys should be -rw------- (600)
# .ssh directory should be drwx------ (700)

# Fix if needed
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Browser never opens for Cloudflare auth

# On CLIENT
cloudflared access login ssh.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Session expired / keeps asking to authenticate

# On CLIENT
cloudflared access logout
ssh myserver
Enter fullscreen mode Exit fullscreen mode

Verify everything is working

# On CLIENT - Connect with verbose output
ssh -v myserver
Enter fullscreen mode Exit fullscreen mode

Complete Configuration Files

Server: docker-compose.yml

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
    network_mode: host
Enter fullscreen mode Exit fullscreen mode

Server: .env

TUNNEL_TOKEN=eyJhIjoiNWFiY2...your-full-token-from-cloudflare...
Enter fullscreen mode Exit fullscreen mode

Client: ~/.ssh/config

Host myserver
    HostName ssh.yourdomain.com
    User your-linux-username
    ProxyCommand cloudflared access ssh --hostname %h
    IdentityFile ~/.ssh/id_ed25519
Enter fullscreen mode Exit fullscreen mode

Summary Checklist

Server Setup

  • [ ] SSH server installed and running
  • [ ] Cloudflare tunnel created in dashboard
  • [ ] Docker compose file created with network_mode: host
  • [ ] Tunnel running (docker compose up -d)
  • [ ] Public hostname added (Type: SSH, URL: localhost:22)
  • [ ] Access application created with email policy
  • [ ] Client's public key added to ~/.ssh/authorized_keys

Client Setup

  • [ ] SSH client installed
  • [ ] cloudflared installed
  • [ ] SSH key generated (~/.ssh/id_ed25519)
  • [ ] SSH config created with ProxyCommand and IdentityFile
  • [ ] Public key copied to server

Test

  • [ ] ssh myserver connects successfully
  • [ ] No password prompt (key auth working)

Top comments (0)