Skip to main content

Make OpenClaw private with a NetBird mesh VPN

· 18 min read
Rafael Fernandes
NLP Engineer & Tech Writer at WiLine
Share:
OpenClaw+NetBird

In Article 2 we put Caddy in front of OpenClaw for HTTPS, and in Article 3 we added a Telegram channel. The gateway works — but Caddy is still listening on 0.0.0.0, reachable by anything that can route to the box. This is the chave de ouro for the series: we join the server and your laptop to a NetBird mesh, repoint openclaw.local at the mesh IP, and close the public ports. The same https://openclaw.local/chat URL keeps working — but only for your devices. Every command and error below is from the actual run.

Reproducibility

Continues from Articles 1–3 on the same WEC Instance — Ubuntu, OpenClaw behind Caddy 2. We use managed NetBird (the free tier at app.netbird.io) with agent 0.73.2 on both the server (Linux, kernel WireGuard) and the client (macOS, userspace). NetBird is WireGuard-based and open source; you can also self-host the control plane, which is a bigger topic for another day.

How NetBird works (and what you'll build)

NetBird has two parts, and the distinction is the whole point:

  1. The control plane (NetBird's managed management + signal service) — this is the only thing with a public presence. Every device that wants to join authenticates to it (with your setup key or SSO login), and it brokers the exchange of each peer's WireGuard public key.
  2. The data plane — once authenticated, peers talk directly to each other, end-to-end encrypted over WireGuard. Your actual traffic never flows through a public port you manage; the control plane just did the introductions.

So you're building a mesh where Peer A (your laptop) reaches Peer B (the OpenClaw box) by its private 100.x address — and openclaw.local points there, with Caddy bound to that interface only. The public/LAN side has nothing listening.

The key idea for later: the mesh isn't limited to two peers. Authenticate once, and every new service — Ollama, a VDI, WordPress — joins as another peer with its own 100.x address, reachable privately with no public ports to open. You set up the auth model once; you add services forever.

The public internet only ever reaches NetBird's control plane — and that just authenticates peers, it never carries your app traffic. Your services aren't "firewalled off" so much as invisible: OpenClaw has no public IP, so there's simply no route to it from the internet. The only way in is to be an authenticated peer on the mesh.

In this tutorial we wire up the first two peers — your laptop and the OpenClaw box. The other services make the point: once auth is set up, Ollama, a VDI, WordPress each just join the same mesh as another 100.x peer — no public ports, no per-service exposure.

Stand up a NetBird control plane

Every NetBird mesh has two roles: peers — your machines, i.e. the OpenClaw box from Parts 1–3 and your laptop — and a control plane that authenticates those peers and issues their join keys. You already have the peer (your OpenClaw box); now you need a control plane for it to join. Two ways to get one:

  • Option A — self-host it on WiLine (recommended). Deploy WEC's NetBird Marketplace template: a dedicated WEC Instance that runs your own NetBird control plane — dashboard, trusted certificate, zero shell. Now your entire private network lives on WiLine — the OpenClaw box and its control plane — with no third-party dependency.
  • Option B — managed NetBird (no extra box). Don't want to run your own control plane? Use the free tier at app.netbird.io — sign in and you're done; the control plane is NetBird's cloud. Then skip to Enroll your machines.
Two boxes, on purpose — and your OpenClaw box is untouched

With Option A you'll run two instances: your OpenClaw box (Parts 1–3, unchanged) and a second, dedicated control-plane box (this template). That's the correct topology — a control plane should be its own machine, never sharing with a workload. With Option B there's only your one OpenClaw box. Either way you never rebuild the OpenClaw box — in Enroll your machines you just install the NetBird agent on it and it joins the mesh as a peer.

The steps below set up Option A. WEC runs the install; you give it an email and a public IP, and a few minutes later you have your own NetBird dashboard on https://<your-ip>.apps.wiline.cloud with a valid certificate.

1. Get a public (Elastic) IP

The dashboard needs a public address. Under Networks → Elastic IPs, allocate a new IP or pick a free one, and note its value (ours: 108.60.112.165). It must be in the VPC and zone you'll deploy into. Full steps: Configure Elastic IPs.

An available Elastic IP in Networks → Elastic IPs

2. Deploy a VM with the template

This new instance becomes your NetBird control plane — a separate box from your OpenClaw instance, which stays untouched. Open Deploy → Deploy a VM (full walkthrough: Deploy a Virtual Machine). The choices that matter here:

  • Network — pick the network in the same VPC/zone as your Elastic IP (we used Local Subnet in Princeton-VPC), so the IP can attach afterward.
  • Template → Marketplace → Ubuntu 24.04 LTS Netbird.
  • Template Configuration — your Let's Encrypt email and the Elastic IP from step 1 as the external address.

Template Configuration — Let&#39;s Encrypt email and the Elastic IP

Finish the wizard and deploy.

3. Attach the Elastic IP to the new instance

The IP isn't bound at deploy time — you attach it once the VM is up. Go to Networks → Elastic IPs → your IP → ⋯ → Allocate, and select your new instance (netbird-tutorial). Only VMs in the same VPC appear in the list.

Allocating the Elastic IP to the new VM

4. Wait for first boot, then verify it's up

The instance shows Running in Compute → Instances within a minute — but the template keeps working for a few minutes after that, pulling and starting the NetBird containers and bringing up the dashboard. Don't expect the URL to answer immediately.

Check from your laptop over HTTPS (not ping — ICMP is firewalled off, so a failed ping doesn't mean it's down):

curl -skS -m 10 -o /dev/null -w "%{http_code}\n" https://<your-ip>.apps.wiline.cloud
  • 000 / connection refused → still provisioning; wait a minute and retry.
  • 301 then 200 → it's up (HTTP redirects to HTTPS).

To watch it directly, SSH in (ssh ubuntu@<your-ip>) and confirm the containers are running and the ports are bound:

cloud-init status
sudo docker ps
sudo ss -tlnp | grep -E ':(80|443)'

It's ready once netbird-dashboard, netbird-traefik, and netbird-server show Up and 80/443 are listening.

5. Open the dashboard and create your account

Open https://<your-ip>.apps.wiline.cloud — click through the certificate warning if you get one (same self-signed-cert click-through as Article 2).

This is your own NetBird control plane, running on your WEC Instance. On the first visit it asks you to create the admin account — enter a name, email, and password, click Create, then sign in with those same credentials. (That setup screen appears only once; afterwards the dashboard shows a normal Sign in page.)

You land on the NetBird dashboard — your private control plane on your own domain:

The self-hosted NetBird dashboard

Click Add Peer and NetBird hands you the enrollment command for your server — note the --management-url pointing at your own domain:

Add Peer on the self-hosted dashboard — note the --management-url flag

curl -fsSL https://pkgs.netbird.io/install.sh | sh
netbird up --management-url https://<your-ip>.apps.wiline.cloud

(That second command opens a browser SSO login. For a headless server, add --setup-key <KEY> from your dashboard's Setup Keys page instead — same as the managed flow, just with --management-url added.)

This is the dashboard where you create the setup keys that enroll machines — which is exactly what we do next.

Now enroll your machines

You have a control plane (your own template dashboard, or managed NetBird). Now put the two machines on the mesh — the OpenClaw box from Parts 1–3 and your laptop — and then close the box's public ports. These steps are the same no matter which control plane you picked.

Where your setup key comes from

The commands below use managed NetBird (app.netbird.io) as the example. If you deployed the WEC template instead, it's identical except: create the setup key in your own dashboard (https://<your-ip>.apps.wiline.cloud) and add --management-url https://<your-ip>.apps.wiline.cloud to netbird up, so the agent points at your server instead of the managed cloud.

Prerequisites: OpenClaw running behind Caddy from Articles 1–3 (reachable at https://openclaw.local), a control plane from the step above, and shell access to the box plus admin on your laptop.

Step 1 — Install the NetBird agent on the OpenClaw box

This is the box from Parts 1–3 (a WEC Instance) — we're enrolling it as a peer, which is separate from any control-plane server. NetBird ships a one-line installer:

curl -fsSL https://pkgs.netbird.io/install.sh | sh

Gotcha (real one): on Ubuntu the install triggers the needrestart dialog — a blue "Which services should be restarted?" box. Tab between buttons can get swallowed by some SSH/terminal setups; use arrow keys to move, Space to toggle, Enter to confirm (<Ok> is the default). The package is already in place by the time this box appears, so even if you Ctrl+C out of it, the agent is installed — re-running the installer just tells you so:

NetBird seems to be installed already, please remove it before proceeding

(If you re-run the installer after this tutorial, you'll instead see NetBird service is running, please stop it before proceeding — the agent's already up, so there's nothing to reinstall.)

Confirm the version:

netbird version
# 0.73.2

Step 2 — Create a setup key and join the OpenClaw box

A setup key is the token that enrolls a device into your mesh — ideal for a headless server (no browser SSO needed).

First time in the dashboard? Skip the detours

A few things will try to send you the wrong way on a fresh account — here's the straight line:

  • No account yet? Create one first (sign in with Google/GitHub/email). You can't generate a key until you're in.
  • The onboarding wizard ("Add your first resource" — Single IP / Entire Subnet / Domain) pops up immediately. Skip it. Resources are for exposing subnets/domains; we just need two plain peers, so it's not needed.
  • The orange "Add Peer" button opens the SSO install flow (download the app, sign up with email, connect from the tray). That's meant for a desktop with a browser — not a headless server. Don't follow it.
  • Instead, go straight to the Setup Keys page: https://app.netbird.io/setup-keys.
  1. Open https://app.netbird.io/setup-keys.
  2. Create Setup Key → name openclaw-tutorial, toggle Reusable (so it works for the laptop too), defaults otherwise.
  3. Copy the key.

Creating a reusable setup key in the NetBird dashboard

Connect the VM (replace with your key):

sudo netbird up --setup-key <YOUR_SETUP_KEY>
Treat the setup key as a secret

Anyone with a reusable key can add devices to your network. Don't paste it into chats or commits, and delete it from the dashboard once your devices are enrolled (the existing peers stay connected).

Check the connection:

netbird status

netbird status on the VM — Management &amp; Signal Connected, mesh IP 100.87.239.229, Peers count 0/0

That NetBird IP (100.87.239.229) is the address everything below points at. Peers count: 0/0 is expected — nothing else has joined yet.

Step 3 — Join your laptop

Install the NetBird client on whatever you're using, then connect with the same reusable setup key. The connect command (netbird up --setup-key …) is identical on every OS — only the install differs.

We ran this on macOS

The captured output below is from a Mac. The Windows and Linux tabs give the equivalent install; the netbird up, netbird status, and ping steps are the same everywhere (Windows uses ping -n instead of -c).

Install via Homebrew, then start the service and connect:

brew install netbirdio/tap/netbird
sudo netbird service install
sudo netbird service start
netbird up --setup-key <YOUR_SETUP_KEY>

You may see a one-off gRPC warning as the daemon's socket comes up a beat after the client calls it — it ends in Connected, so it's harmless:

Output
WARNING: ... dial unix /var/run/netbird.sock: connect: no such file or directory
Connected

Verify the two nodes see each other, and that the tunnel carries traffic:

netbird status
ping -c 3 100.87.239.229 # Windows: ping -n 3 100.87.239.229

Mac netbird status showing Peers count 1/1 Connected, plus a successful ping to the VM&#39;s mesh IP

Peers count: 1/1 — that peer is the VM. In the dashboard, both machines now show up under Peers (because we enrolled them with a setup key rather than an SSO login, they appear under Servers rather than User Devices):

The VM and laptop both connected as peers in the NetBird dashboard

Why ~200 ms?

This path is going through a NetBird relay (note Interface type: Userspace on the Mac). It's fine for a web UI, and NetBird will often upgrade to a direct peer-to-peer link when the networks allow. Latency to a relayed peer is not a red flag.

Step 4 — Repoint openclaw.local at the mesh IP

From Article 2, your hosts file maps openclaw.local to the VM's LAN IP (10.80.4.212). Swap it for the mesh IP (100.87.239.229). The file path and edit command differ by OS:

sed on macOS needs the empty -i '':

grep openclaw /etc/hosts
# 10.80.4.212 openclaw.local
sudo sed -i '' 's/^10\.80\.4\.212 openclaw\.local/100.87.239.229 openclaw.local/' /etc/hosts
grep openclaw /etc/hosts
# 100.87.239.229 openclaw.local

Test over the mesh — before touching any ports:

curl -kI https://openclaw.local/
Output
HTTP/2 200
...
via: 1.1 Caddy

via: 1.1 Caddy over the mesh IP — the chat now flows through NetBird. Load https://openclaw.local/chat?session=main in the browser to confirm it behaves exactly as before. The TLS cert still matches because the hostname is unchanged; only the IP behind it moved.

The OpenClaw chat loading over the mesh at https://openclaw.local

Step 5 — Close the public ports

Here's the state we're fixing. On the VM:

sudo ss -tlnp | grep -E ':(80|443)'
Output — listening on 0.0.0.0 (every interface)
LISTEN 0 4096 0.0.0.0:443 ... docker-proxy
LISTEN 0 4096 0.0.0.0:80 ... docker-proxy

Caddy listens on 0.0.0.0 — every interface, including the LAN (and any NAT in front of it).

The ufw trap

Your first instinct is ufw deny 443. It won't work. Caddy's ports are published by Docker, which writes its own iptables rules that bypass ufw's INPUT chain. You'd add ufw rules, feel safe, and 443 would stay wide open. On this box ufw was even inactive — and enabling it would have changed nothing for the Docker-published ports.

The reliable fix is to bind the published ports to the mesh IP instead of 0.0.0.0, so Caddy only ever listens on wt0. Edit the Caddy service in docker-compose.override.yml (the override from Article 2):

cd /home/ubuntu/openclaw
cp docker-compose.override.yml docker-compose.override.yml.bak

Change the ports block from:

ports:
- "80:80"
- "443:443"

to bind to the mesh IP:

ports:
- "100.87.239.229:80:80"
- "100.87.239.229:443:443"

Recreate just Caddy and re-check:

docker compose up -d caddy
sudo ss -tlnp | grep -E ':(80|443)'

ss after the change — Caddy now listening only on the mesh IP 100.87.239.229, no longer on 0.0.0.0

The ports are gone from 0.0.0.0 — they live on the mesh interface only.

Startup ordering

Because the bind targets the mesh IP, NetBird must be up before Caddy starts (e.g. after a reboot, wt0 needs its IP before Docker binds to it). The restart: unless-stopped policy already covers this — Caddy retries until the interface exists — but it's worth knowing if you ever see Caddy crash-looping right after boot.

Step 6 — Prove the lockdown

Both directions, from the actual run.

Still reachable over the mesh (laptop):

curl -kI https://openclaw.local/
# HTTP/2 200 ... via: 1.1 Caddy

No longer reachable on the LAN (VM, hitting its own LAN IP):

curl -kI --connect-timeout 5 --resolve openclaw.local:443:10.80.4.212 https://openclaw.local/

On the VM, curl to the old LAN IP now returns Connection refused

That's the result: same URL, reachable only through your mesh, refused everywhere else.

Cleanup

Delete the reusable setup key now that both devices are enrolled — Setup Keys → delete in the dashboard. Your peers stay connected; you're just closing the door you used to add them. To remove a device later, delete it under Peers → User Devices and run netbird down on that machine.

Troubleshooting (real)

  • needrestart dialog won't take Tab. Use arrow keys / Space / Enter. The package is already installed by the time the box shows.
  • gRPC netbird.sock warning on first up. The daemon's socket lags the client by a beat. If it ends in Connected, ignore it.
  • ufw rules don't block the gateway. Docker-published ports bypass ufw — bind the port to a specific IP (as above) or use Docker's own DOCKER-USER chain.
  • Caddy crash-loops after reboot. It started before wt0 had the mesh IP. restart: unless-stopped recovers it; or order Docker after netbird.service.
  • High ping to a peer. You're relayed, not direct — functional, just slower. Check Interface type and NetBird's peer detail for the connection type.

What's next

That closes the Self-hosting OpenClaw series:

  1. Deploy OpenClaw on a WEC Instance via Docker Compose
  2. Secure OpenClaw with a Caddy reverse proxy + HTTPS
  3. Add a Telegram channel to OpenClaw
  4. Make OpenClaw private with a NetBird mesh VPN ← you are here

You now have a self-hosted agent that's encrypted in transit, paired to your devices, reachable from your phone, and invisible to the public internet — all on one small WEC Instance.

Continue the journey

Ready for an agent that remembers? Start the Self-hosting Hermes series — Self-host the Hermes Agent with persistent memory — deployed on this same WEC Instance, with context that survives restarts.