Skip to content

Adding a roaming node

This guide walks through adding a roaming node: Ubuntu at home (or anywhere with a changing public IP), bootstrap SSH through Cloudflare Tunnel, and WireGuard that always dials out to your public static hub(s).

The example uses the prod environment, host name roaming-1, and hostname roaming-1.example.com.

Where you see ENV=<env>, replace with your environment name (for example prod).

You need a working static environment, operator SSH access, a Cloudflare zone, an Ubuntu roaming VM, and an external Postgres instance for that environment (configured in the vault before the first deploy).

You need:

  1. A working environment (for example prod) with at least one public static node (Adding a public node or Deploy to production).
  2. The same operator SSH key already installed on your static nodes (~/.ssh/<project>-<env>).
  3. A domain on Cloudflare (same account you will use for Zero Trust).
  4. An Ubuntu 26.04 amd64 machine for the roaming node (VM, NUC, NAS, etc.).
  5. An external Postgres instance with owner role hosted outside of the Supercompute cloud.

Prove SSH to a static node in your environment first:

Terminal window
task ssh ENV=prod NODE=static-1

Add a block like this under nodes.hosts in inventories/<env>/hosts.yml:

roaming-1:
roaming: true
bootstrap_ssh_host: "roaming-1.example.com"
ssh_ed25519_sha256: "SHA256:pRR2cezc6UOTGq+vTrVwPFCFKlHRKuuV/OL/9/c3e+M"
private_address: 10.217.79.21

No public_ip. No node_lima_guest.

Install Ubuntu, create the ops user, and confirm SSH listens on port 22.

On the roaming machine (local console or temporary LAN SSH):

  1. Install Ubuntu 26.04 amd64.
  2. Create user ops, install your Mac operator public key, passwordless sudo — same commands as Adding a public node Step 3.
  3. Confirm sshd listens on port 22:
Terminal window
sudo ss -tlnp | grep ':22'
  1. Confirm outbound internet works (especially UDP — WireGuard needs it). Do not forward inbound UDP 51830 on your router.

Step 2: Create a Cloudflare Tunnel (dashboard)

Section titled “Step 2: Create a Cloudflare Tunnel (dashboard)”

Create a Zero Trust tunnel that will carry bootstrap SSH to the roaming VM.

Do this in a browser while logged into Cloudflare.

  1. Open Cloudflare OneNetworksConnectorsCloudflare Tunnels (menu label may appear as Tunnels under Networks).
  2. Click Create a tunnel.
  3. Choose connector type CloudflaredNext.
  4. Tunnel name: pick something durable (example home-lab-ssh). Click Save tunnel.
  5. On Install connector, select Debian/Ubuntu (or your OS). Cloudflare shows an install command with a token. Copy that full command — you will run it on the roaming VM in Step 3.

Save in your password manager:

Item Where it lives
Tunnel name Cloudflare dashboard
Install command / token Shown once on the install step; also ends up in /etc/cloudflared/ on the VM after install
  1. Leave the browser tab open; you will return to add a Published application route after the connector is healthy.

Official reference: Connect to SSH with client-side cloudflared.

Step 3: Install cloudflared on the roaming VM

Section titled “Step 3: Install cloudflared on the roaming VM”

Run the tunnel install command on the roaming VM and confirm it connects.

SSH or console on the roaming VM. Paste the exact install command from Step 2 (it includes your tunnel token).

Example shape (yours will differ):

Terminal window
sudo cloudflared service install eyJhIjoi…

Then:

Terminal window
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

Back in the Cloudflare dashboard, the tunnel should show Healthy / connected. Click Next until you reach route configuration (or open the tunnel → Routes tab).

On the VM you can also verify:

Terminal window
cloudflared tunnel list
cloudflared tunnel info home-lab-ssh

Save: note the tunnel UUID if shown (cloudflared tunnel list) — useful for DNS CLI later.

Step 4: Publish SSH on a hostname (Published application)

Section titled “Step 4: Publish SSH on a hostname (Published application)”

Route a public hostname to SSH on the roaming VM through the tunnel.

Still in Cloudflare One → Tunnels → select your tunnel.

  1. Open the Routes tab (older UI: Public Hostname).
  2. Click Add routePublished application (wording may be Add published application).
  3. Fill the form:
Field Example Notes
Subdomain roaming-1 Becomes roaming-1.example.com
Domain example.com Must be a zone in this account
Path (leave empty)
Type / Service SSH Not HTTP
URL / Service URL localhost:22 SSH on this machine where cloudflared runs

If the UI asks for a full URI, use ssh://localhost:22.

Wrong: Service URL = 22 alone. Right: type SSH + localhost:22.

  1. Click Add route / Save route.

Cloudflare usually creates a proxied CNAME for roaming-1.example.com<tunnel-id>.cfargotunnel.com. Confirm under your zone DNSRecords.

If DNS is missing, from a machine with tunnel credentials:

Terminal window
cloudflared tunnel route dns home-lab-ssh roaming-1.example.com

Restrict who can reach the tunnel hostname before SSH reaches the VM.

Access controls who can use the hostname before SSH reaches your VM.

  1. Cloudflare OneAccess controlsApplications.
  2. Add an applicationSelf-hosted.
  3. Application domain: roaming-1.example.com (same hostname as Step 4).
  4. Policies → add a rule:
    • Action: Allow
    • Include: your email (or an IdP group you use)
  5. Save.

First SSH from the Mac may open a browser for Access login. Complete login until ssh … true works without a password prompt (key-based ops login after Access).

Save: nothing goes into the Supercompute repo for Access — credentials are your Cloudflare account / IdP. Optional later: service tokens for non-interactive automation (not required for task up from your Mac).

Step 6: Mac — cloudflared and SSH config

Section titled “Step 6: Mac — cloudflared and SSH config”

Configure your Mac to reach the roaming VM through the tunnel.

On your Mac:

Terminal window
command -v cloudflared

If missing, run task setup (installs cloudflared on macOS via Homebrew) or brew install cloudflared.

Add to ~/.ssh/config (adjust key path to match your provider):

Host roaming-1.example.com
User ops
IdentityFile ~/.ssh/sc-prod
IdentitiesOnly yes
ProxyCommand cloudflared access ssh --hostname %h

bootstrap_ssh_host in hosts.yml must be exactly this hostname (the Host name / DNS name, not an alias).

Confirm tunnel SSH from your Mac works and record the VM host-key fingerprint.

Terminal window
ssh roaming-1.example.com true

Fix Access policy, DNS, or the published SSH route until this succeeds.

Record the VM host key fingerprint for hosts.yml:

Terminal window
ssh roaming-1.example.com 'sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub'

Copy the full SHA256:… value into your password manager.

Check OK?
Tunnel Healthy in Cloudflare
Published route: SSHlocalhost:22
DNS record for roaming-1.example.com
Access application allows your identity
ssh roaming-1.example.com true from Mac
Host key fingerprint recorded

Add the roaming host, tunnel hostname, and fingerprint to your hosts inventory.

Edit inventories/<env>/hosts.yml. Add roaming-1 under nodes.hosts with the next free mesh IP in your CIDR (example prod 10.217.79.0/24.21 if .11 and .12 are statics):

roaming-1:
roaming: true
bootstrap_ssh_host: "roaming-1.example.com"
ssh_ed25519_sha256: "SHA256:pRR2cezc6UOTGq+vTrVwPFCFKlHRKuuV/OL/9/c3e+M"
private_address: 10.217.79.21

To add roaming-2, repeat this guide with a new hostname and mesh IP.

Open inbound UDP 51830 on static hubs so roaming nodes can dial out to them.

Roaming nodes dial out to public statics. Each static VM must allow inbound UDP 51830 from the internet (or wide enough sources) — not only your Mac /32, because the roaming public IP changes.

TCP 22 on statics is separate (Mac bootstrap to statics). Roaming bootstrap uses Cloudflare only.

Initialize secrets if this is a new environment, then join the roaming node to the mesh.

If this is a new environment, run task vault-init ENV=<env>, then set vault_database_url with task vault-edit ENV=<env> before the first task up. Supercompute needs a Postgres database with owner role hosted outside of the Supercompute cloud. Existing prod: update hosts.yml only, then task up ENV=prod.

Terminal window
task up ENV=prod

up reaches roaming-1 via Cloudflare when the mesh is down, then over WireGuard after join. Roaming always initiates to the static hub; Mac↔roaming traffic relays through static-1 (first static by name).

Confirm you can reach the roaming node over WireGuard.

Terminal window
task ssh ENV=prod NODE=roaming-1

Optional latency check from a node to the Mac mesh peer:

Terminal window
ping -c 10 -I scwg0 10.217.79.1

Optional on the roaming node:

Terminal window
systemctl status supercompute-roaming-dial.timer
Secret / artifact Purpose
Operator SSH private key Mac → nodes (same as static nodes)
Cloudflare tunnel install token / credentials on VM Keeps tunnel connected after reboot
ssh_ed25519_sha256 for roaming-1 Inventory + known_hosts contract
Vault password + encrypted vault.yml Mesh keys

Supercompute does not store Cloudflare API tokens or tunnel credentials in the repository.

Go back to Deploy to production if you started there, or add another roaming host by repeating this guide with roaming-2.