Prototype KubeClaw is a learning project exploring what a secure, scalable Kubernetes environment for AI agents looks like. It is not production ready and not beta. Expect breaking changes and unreviewed assumptions. Read the project status.

Cloudflare Tunnel Setup

Create the tunnel, route SSH through it, and protect it with a Cloudflare Access policy.

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:

PathWhen to useHow it works
Admin node (temporary)Initial setup, before tunnel is readyJump host with public IPv6; SSH via ProxyJump
Cloudflare Tunnel (permanent)After tunnel is configuredcloudflared 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

  1. In the left sidebar, navigate to Networks > Tunnels
  2. Click Create a tunnel
┌──────────────────────────────────────────────────────────────────┐
│  Networks > Tunnels                                              │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │                   Create a tunnel                          │  │
│  │                                                            │  │
│  │  Select your tunnel type:                                  │  │
│  │                                                            │  │
│  │  ┌─────────────────────┐   ┌─────────────────────┐       │  │
│  │  │ ● Cloudflared       │   │ ○ WARP Connector     │       │  │
│  │  │   (recommended)     │   │                      │       │  │
│  │  └─────────────────────┘   └─────────────────────┘       │  │
│  │                                                  [ Next ] │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘
  1. Select Cloudflared and click Next
  2. Enter a tunnel name, e.g. kubeclaw or hetzner-cluster
┌──────────────────────────────────────────────────────────────────┐
│  Name your tunnel                                                │
│                                                                  │
│  Tunnel name:  ┌──────────────────────────────┐                 │
│                │ kubeclaw                      │                 │
│                └──────────────────────────────┘                 │
│                                                                  │
│                                               [ Save tunnel ]   │
└──────────────────────────────────────────────────────────────────┘
  1. 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 — so cloud-init auto-installs the tunnel on the master node
  • Manual install — if the infrastructure is already running

Click Next to proceed to the hostname configuration.

Step 4: Add a Public Hostname for SSH

This step maps a subdomain to the SSH service on your master control node.

  1. You should now be on the Route tunnel screen, or navigate to your tunnel’s Public Hostname tab
  2. Click Add a public hostname
  3. 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 ]  │
└──────────────────────────────────────────────────────────────────┘
FieldValueNotes
SubdomainconsoleOr any name you prefer (e.g. ssh, cluster)
DomainYour Cloudflare-managed domainMust be a domain with Cloudflare DNS
Path(leave empty)Not used for SSH
TypeSSHFrom the dropdown
URLlocalhost:22The tunnel connector runs on the master node, so SSH is on localhost
  1. 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.

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).

  1. In the left sidebar, go to Access > Applications
  2. Click Add an application
  3. 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 ]      │
└──────────────────────────────────────────────────────────────────┘
  1. Click Next to configure a policy
  2. Create an Allow policy:
┌──────────────────────────────────────────────────────────────────┐
│  Add Policies                                                    │
│                                                                  │
│  Policy name:  ┌──────────────────────────┐                     │
│                │ Allow Admin              │                     │
│                └──────────────────────────┘                     │
│                                                                  │
│  Action:  ┌──────────────────────────┐                          │
│           │ Allow                 ▼  │                          │
│           └──────────────────────────┘                          │
│                                                                  │
│  Configure rules:                                                │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  Include                                                   │  │
│  │  Selector:  ┌─────────────────┐  Value: ┌───────────────┐ │  │
│  │             │ Emails       ▼  │         │ you@email.com │ │  │
│  │             └─────────────────┘         └───────────────┘ │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│                                                    [ Next ]      │
└──────────────────────────────────────────────────────────────────┘
FieldValue
Policy nameAllow Admin
ActionAllow
Include selectorEmails
Include valueYour email address
  1. Click Next, review, and Save

After completing the Cloudflare setup, continue with Infrastructure (OpenTofu) to configure the tunnel token and domain in 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+)         │
                                          └──────────────────────┘
TargetSSH commandRoute
Master control nodessh console.yourdomain.orgProxyCommand → Cloudflare → cloudflared → localhost:22
Replica control nodessh control-02ProxyCommand → Cloudflare → master → ProxyJump → 10.0.0.3
Worker nodessh worker-01ProxyCommand → 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