Cloudflare Tunnel Setup
This guide walks through creating and configuring a Cloudflare Tunnel that provides secure SSH access to your KubeClaw cluster. The tunnel replaces the temporary admin node as the permanent access path – no open ports, no public SSH, outbound-only connectivity.
Why Cloudflare Tunnel?
KubeClaw nodes have no public IPv4 and no inbound SSH ports. Access works through one of two paths:
| Path | When to use | How it works |
|---|---|---|
| Admin node (temporary) | Initial setup, before tunnel is ready | Jump host with public IPv6; SSH via ProxyJump |
| Cloudflare Tunnel (permanent) | After tunnel is configured | cloudflared on the master node connects outbound to Cloudflare’s edge; SSH proxied via ProxyCommand cloudflared access ssh on your local machine |
After the tunnel is working, you disable the admin node (enable_admin_node = false in terraform.tfvars) and all SSH flows through Cloudflare.
Prerequisites
- A Cloudflare account (free tier is sufficient)
- A domain added to Cloudflare (Cloudflare must be the DNS provider)
- Infrastructure provisioned with
tofu apply(the master control node must be running)
Step 1: Open Zero Trust Dashboard
Go to https://one.dash.cloudflare.com and log in. This opens the Cloudflare Zero Trust dashboard (formerly Cloudflare for Teams).
┌──────────────────────────────────────────────────────────────────┐
│ Cloudflare | Zero Trust │
├──────────────┬───────────────────────────────────────────────────┤
│ │ │
│ Home │ Zero Trust Overview │
│ Analytics │ │
│ Risk Score │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ Users │ │ Tunnels │ │ Policies │ │
│ Access ► │ │ 0 │ │ 0 │ │ 0 │ │
│ Gateway │ └──────────┘ └──────────┘ └──────────┘ │
│ Networks ► │ │
│ ... │ │
│ │ │
└──────────────┴───────────────────────────────────────────────────┘
Step 2: Create a Tunnel
- In the left sidebar, navigate to Networks > Tunnels
- Click Create a tunnel
┌──────────────────────────────────────────────────────────────────┐
│ Networks > Tunnels │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Create a tunnel │ │
│ │ │ │
│ │ Select your tunnel type: │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
│ │ │ ● Cloudflared │ │ ○ WARP Connector │ │ │
│ │ │ (recommended) │ │ │ │ │
│ │ └─────────────────────┘ └─────────────────────┘ │ │
│ │ [ Next ] │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
- Select Cloudflared and click Next
- Enter a tunnel name, e.g.
kubeclaworhetzner-cluster
┌──────────────────────────────────────────────────────────────────┐
│ Name your tunnel │
│ │
│ Tunnel name: ┌──────────────────────────────┐ │
│ │ kubeclaw │ │
│ └──────────────────────────────┘ │
│ │
│ [ Save tunnel ] │
└──────────────────────────────────────────────────────────────────┘
- Click Save tunnel
Step 3: Copy the Tunnel Token
After saving, Cloudflare shows the connector installation instructions. The page displays install commands for various platforms. You need the token value from the install command.
┌──────────────────────────────────────────────────────────────────┐
│ Install and run a connector │
│ │
│ Choose your environment: │
│ [ Debian ] [ Docker ] [ macOS ] [ Windows ] │
│ │
│ Install and run a connector: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ sudo cloudflared service install eyJhIjoiY2Y...long-token │ │
│ └────────────────────────────────────────────────────────────┘ │
│ 📋 │
│ │
│ The token is: eyJhIjoiY2Y... │
│ │
│ [ Next ] │
└──────────────────────────────────────────────────────────────────┘
Copy the token (the eyJ... string). You will need it in two places:
terraform.tfvars— socloud-initauto-installs the tunnel on the master node- Manual install — if the infrastructure is already running
Step 4: Add a Public Hostname for SSH
This step maps a subdomain to the SSH service on your master control node.
- You should now be on the Route tunnel screen, or navigate to your tunnel’s Public Hostname tab
- Click Add a public hostname
- Fill in the hostname configuration:
┌──────────────────────────────────────────────────────────────────┐
│ Public Hostnames > Add a public hostname │
│ │
│ Public hostname │
│ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ console │ . │ yourdomain.org ▼ │ │
│ │ (subdomain) │ │ (domain) │ │
│ └──────────────┘ └─────────────────────────┘ │
│ │
│ Path (optional): ┌──────────────────────────┐ │
│ │ │ │
│ └──────────────────────────┘ │
│ │
│ Service │
│ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ SSH ▼ │ │ localhost:22 │ │
│ │ (type) │ │ (URL) │ │
│ └──────────────┘ └─────────────────────────┘ │
│ │
│ [ Save hostname ] │
└──────────────────────────────────────────────────────────────────┘
| Field | Value | Notes |
|---|---|---|
| Subdomain | console | Or any name you prefer (e.g. ssh, cluster) |
| Domain | Your Cloudflare-managed domain | Must be a domain with Cloudflare DNS |
| Path | (leave empty) | Not used for SSH |
| Type | SSH | From the dropdown |
| URL | localhost:22 | The tunnel connector runs on the master node, so SSH is on localhost |
- Click Save hostname
The resulting hostname (e.g. console.yourdomain.org) is what you’ll use as cloudflare_tunnel_domain in terraform.tfvars and as the SSH Host in your SSH config.
Step 5: Create an Access Application (Optional but Recommended)
Cloudflare Access adds browser-based authentication before the SSH connection is established. Without it, anyone who knows your tunnel hostname can attempt SSH connections (still protected by SSH keys, but Access adds a second layer).
- In the left sidebar, go to Access > Applications
- Click Add an application
- Select Self-hosted
┌──────────────────────────────────────────────────────────────────┐
│ Access > Applications > Add an application │
│ │
│ Application Configuration │
│ │
│ Application name: ┌─────────────────────────┐ │
│ │ KubeClaw SSH Console │ │
│ └─────────────────────────┘ │
│ │
│ Session Duration: ┌─────────────────────────┐ │
│ │ 24 hours ▼ │ │
│ └─────────────────────────┘ │
│ │
│ Application domain: │
│ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ console │ . │ yourdomain.org ▼ │ │
│ └──────────────┘ └─────────────────────────┘ │
│ │
│ [ Next ] │
└──────────────────────────────────────────────────────────────────┘
- Click Next to configure a policy
- Create an Allow policy:
┌──────────────────────────────────────────────────────────────────┐
│ Add Policies │
│ │
│ Policy name: ┌──────────────────────────┐ │
│ │ Allow Admin │ │
│ └──────────────────────────┘ │
│ │
│ Action: ┌──────────────────────────┐ │
│ │ Allow ▼ │ │
│ └──────────────────────────┘ │
│ │
│ Configure rules: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Include │ │
│ │ Selector: ┌─────────────────┐ Value: ┌───────────────┐ │ │
│ │ │ Emails ▼ │ │ you@email.com │ │ │
│ │ └─────────────────┘ └───────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [ Next ] │
└──────────────────────────────────────────────────────────────────┘
| Field | Value |
|---|---|
| Policy name | Allow Admin |
| Action | Allow |
| Include selector | Emails |
| Include value | Your email address |
- Click Next, review, and Save
terraform.tfvars, provision the cluster, and verify connectivity.How SSH Routing Works
After the tunnel is active and the admin node is disabled:
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Your │ │ Cloudflare │ │ Master Control Node │
│ machine │────►│ Edge │────►│ (cloudflared → │
│ (cloudflared│ │ Network │ │ localhost:22) │
│ access ssh)│ │ │ │ │
└──────────────┘ └──────────────┘ └──────────┬───────────┘
│ ProxyJump
┌──────────▼───────────┐
│ Replicas / Workers │
│ (10.0.0.3+) │
└──────────────────────┘
| Target | SSH command | Route |
|---|---|---|
| Master control node | ssh console.yourdomain.org | ProxyCommand → Cloudflare → cloudflared → localhost:22 |
| Replica control node | ssh control-02 | ProxyCommand → Cloudflare → master → ProxyJump → 10.0.0.3 |
| Worker node | ssh worker-01 | ProxyCommand → Cloudflare → master → ProxyJump → 10.0.0.x |
Troubleshooting
Tunnel shows “Inactive” or “Down”
# On the master control node:
sudo systemctl status cloudflared
sudo journalctl -u cloudflared --no-pager -n 50
Common causes:
- Token expired or revoked – create a new tunnel and update
terraform.tfvars - DNS64/NAT64 not working – cloudflared needs outbound connectivity; check with
curl -6 https://cloudflare.com
Browser authentication loop
If ssh console.yourdomain.org keeps opening the browser without connecting:
# Clear cached credentials
cloudflared access login --reset console.yourdomain.org
“connection refused” after authentication
The SSH service on the master node may not be listening on localhost:
# On the master node, verify SSH listens on 127.0.0.1
sudo ss -tlnp | grep 22
The cloud-init template configures UFW to allow SSH from localhost for exactly this reason.
Next Steps
- Infrastructure (OpenTofu) – provision the cluster (if not done yet)
- Server Management (Ansible) – apply updates and hardening
- Kubernetes (kubeadm) – deploy the cluster