← Back to Blog

Securing OpenClaw on a VPS with Docker: A Security-First Setup Guide

When you deploy an AI gateway that sits between users, channels, and model providers, especially one that can broker tool calls, you're not just running "another app." You're creating a trust boundary. This guide walks you through a security-first, repeatable Docker setup on a VPS and explains the reasoning behind each control.

About OpenClaw: OpenClaw is an open-source personal AI assistant that you run on your own infrastructure. Learn more at openclaw.ai · GitHub · Docker Setup Docs

Why Security Matters for AI Gateways

AI gateways like OpenClaw occupy a uniquely sensitive position in your infrastructure. They handle authentication tokens for model providers, route potentially sensitive prompts, and can execute tool calls that interact with external systems. A compromised AI gateway doesn't just leak data; it can act on your behalf.

If you strip the buzzwords, you're protecting four things:

This guide keeps those four controls explicit and easy to verify at every step.

The Security Posture at a Glance

OpenClaw Security Controls Visualization

Control 1 — Exposure (Default: Private)

Goal: Prevent accidental internet access.

Control 2 — Identity (Default: Keys Only)

Goal: Remove the easiest attack path on servers.

Control 3 — Secrets (Default: Least Readable)

Goal: Prevent token leakage through filesystem permissions.

Control 4 — Capabilities (Default: Least Privilege)

Goal: Reduce impact if a channel or token is compromised.

Architecture Overview

Before diving into the steps, understand the traffic flow:

Laptop browser
  └─▸ http://127.0.0.1:18789             (local)
       └─▸ SSH tunnel (encrypted)
            └─▸ VPS 127.0.0.1:18789      (loopback-only)
                 └─▸ Docker gateway      (bind: lan inside Docker network)

Two details matter here:

Prerequisites

Step 1 — Harden SSH (Keys Only, No Root Login)

SSH is the front door to your VPS. The default configuration on most providers allows password login and root access, both easy targets for brute-force attacks. Let's close those paths.

Create a non-root user and grant sudo:

adduser appuser
usermod -aG sudo appuser

Move SSH access to the new user:

mkdir -p /home/appuser/.ssh
chmod 700 /home/appuser/.ssh
cp /root/.ssh/authorized_keys /home/appuser/.ssh/authorized_keys
chmod 600 /home/appuser/.ssh/authorized_keys
chown -R appuser:appuser /home/appuser/.ssh

Lock down SSH configuration in /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Restart SSH to apply:

sudo systemctl restart ssh

What this achieves: Even if someone discovers your VPS IP, password brute-force becomes irrelevant and root is not directly reachable. The attacker would need your private key to get in.

Step 2 — Firewall with Intent

A firewall is your second line of defense. UFW (Uncomplicated Firewall) on Ubuntu makes this straightforward:

sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status

For a stronger stance, restrict SSH to your home public IP only:

sudo ufw delete allow 22/tcp
sudo ufw allow from YOUR_HOME_PUBLIC_IP to any port 22 proto tcp
sudo ufw status

What this achieves: SSH stays reachable for you, but random internet traffic can't even attempt a connection. Combined with key-only auth, you've eliminated the two most common VPS attack vectors.

Defense-in-Depth: Use Your Provider's Firewall Too

UFW is your host-level firewall. But most cloud providers (including Hetzner) offer a network-level firewall at the edge. Enable that as well:

Best practice: Keep the provider firewall rules identical to UFW. This creates a layered defense: if one layer fails, the other still protects you.

Two Safe SSH Access Patterns (and How to Avoid Lockouts)

Restricting SSH to a single home IP is simple and effective, but there's a catch: if your home IP changes, you're locked out. Here are two proven patterns:

Pattern A: SSH Restricted to Home IP

  • Setup: sudo ufw allow from YOUR_HOME_IP/32 to any port 22 proto tcp
  • Pros: Simple, zero additional software
  • Cons: If your home IP changes (ISP rotation, travel, coffee shop), you're locked out

Pattern B: SSH Only via Tailscale (Recommended)

  • Setup: Install Tailscale on both your laptop and the VPS, then restrict SSH using firewall rules on the tailscale0 interface
  • Pros: Works from anywhere (even behind NAT), encrypted mesh network, no lockout risk
  • Cons: Requires Tailscale setup (5 minutes)

No-Lockout Rollout Sequence (Pattern B):

Safety tip: Keep SSH listening on the VPS public interface, and enforce restrictions in firewall rules. If you force SSH to listen only on Tailscale, home-IP fallback cannot help during outages.

Step 3 — Install Docker + Compose v2

Docker provides a repeatable runtime. You can stop, update, and restart without reinstalling dependencies on the host. Compose v2 is the modern standard (the docker compose subcommand, not the legacy Python-based docker-compose).

sudo apt-get update
sudo apt-get install -y docker.io docker-compose-plugin git jq fail2ban
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Verify the installation:

docker --version
docker compose version

Step 4 — Add Intrusion Prevention with Fail2ban

Fail2ban monitors SSH login attempts and automatically bans IPs after repeated failures. It's a simple, effective layer against brute-force attacks.

Enable and start Fail2ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Verify it's protecting SSH:

sudo fail2ban-client status sshd

You should see output like:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

What this achieves: Even with key-only auth, attackers will still try password guessing. Fail2ban detects these attempts and blocks the source IP for a configurable period. A common baseline is bantime=600 and maxretry=5, but always verify your local settings.

Optional: Whitelist Tailscale IPs

If you later adopt Tailscale (Pattern B above), you may want to prevent Fail2ban from accidentally banning your Tailnet IPs. Create /etc/fail2ban/jail.local:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 <YOUR_TAILSCALE_IP_RANGE>

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 5
bantime = 600

Replace <YOUR_TAILSCALE_IP_RANGE> with your Tailscale network range (typically 100.64.0.0/10 for the full CGNAT range, or a more specific subnet like 100.x.y.0/24 for your tailnet). Restart Fail2ban to apply:

sudo systemctl restart fail2ban

Step 5 — Deploy OpenClaw with Docker

cd ~
git clone <OPENCLAW_REPO_URL> openclaw
cd openclaw

If the repo has a setup helper:

./docker-setup.sh

Start the service:

docker compose up -d
docker compose ps

Step 6 — Publish Ports to Localhost Only (Your Biggest Safety Win)

This is arguably the single most important security measure in the entire guide. By default, Docker publishes ports to 0.0.0.0, making services internet-accessible. We bind exclusively to the loopback interface instead.

In your docker-compose.yml for the gateway service:

ports:
  - "127.0.0.1:18789:18789"
  - "127.0.0.1:18790:18790"

Apply the change:

docker compose up -d

Verify the gateway is not public:

docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep -i gateway

✅ You want to see 127.0.0.1:18789->...

🚫 If you see 0.0.0.0:18789->..., the service is internet-facing. Fix it immediately.

Why this matters: You can keep UFW simple because OpenClaw never binds to a public interface on the VPS. Even if UFW is misconfigured, the Docker binding itself prevents external access.

Step 7 — Configure Token Auth and Reduce Tool Blast Radius

Now let's configure OpenClaw itself. First, generate a strong token securely:

# Generate a strong random token
openssl rand -base64 32

Copy the output and store it safely. Then create the configuration file interactively to avoid exposing the token in shell history:

# Create the config directory
docker compose exec -T openclaw-gateway sh -lc 'mkdir -p /home/node/.openclaw'

# Open a shell in the container
docker compose exec openclaw-gateway sh

# Inside the container, create the config file
cat > /home/node/.openclaw/openclaw.json <<'EOF'
{
  "gateway": {
    "mode": "local",
    "bind": "lan",
    "port": 18789,
    "controlUi": {
      "enabled": true,
      "allowInsecureAuth": true
    },
    "auth": {
      "mode": "token",
      "token": "PASTE_YOUR_TOKEN_HERE"
    },
    "remote": {
      "url": "ws://openclaw-gateway:18789",
      "token": "PASTE_YOUR_TOKEN_HERE"
    }
  },
  "tools": {
    "deny": [
      "exec",
      "process",
      "gateway",
      "nodes",
      "canvas",
      "sessions_spawn",
      "sessions_send",
      "sessions_history",
      "sessions_list",
      "agents_list"
    ]
  }
}
EOF

# Exit the container shell
exit

Security Note: Replace PASTE_YOUR_TOKEN_HERE with your actual token by editing the file with a text editor like vi or nano inside the container. This prevents the token from appearing in shell history or process lists.

Safer pattern: Keep token creation and file editing interactive so the secret does not appear in shell history, process arguments, or console logs. Avoid commands that echo tokens back to screen.

Tighten file permissions inside the container:

docker compose exec -T openclaw-gateway sh -lc '
chmod 700 /home/node/.openclaw
chmod 600 /home/node/.openclaw/openclaw.json
'

Restart to pick up the config:

docker compose restart openclaw-gateway

What Each Configuration Section Does

A Note on allowInsecureAuth

In a strict security posture, you'd prefer false. If your browser session disconnects with false, it's acceptable to keep it true only with all four compensating controls in place:

Without these compensating controls, allowInsecureAuth: true would be a real risk.

Optional: Trusted Proxies (Only if Behind a Reverse Proxy)

If you terminate traffic through a reverse proxy (or see warnings about proxy headers), restrict trust to the proxy subnet. First, discover your OpenClaw Docker network subnet:

docker network inspect openclaw_default \
  -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

Then add to your config:

"trustedProxies": ["<docker-subnet-or-proxy-cidr>"]

Rule: Keep the trusted proxy range narrow. Avoid broad ranges and never use 0.0.0.0/0, which would trust the entire internet.

Step 8 — Access the Control UI Privately via SSH Tunnel

With everything bound to loopback, you access the UI through an encrypted SSH tunnel from your laptop:

ssh -i ~/.ssh/your_vps_key \
    -o IdentitiesOnly=yes \
    -N \
    -L 18789:127.0.0.1:18789 \
    -L 18790:127.0.0.1:18790 \
    appuser@YOUR_VPS_IP

Then open in your browser:

Why this is safe: Your browser talks to your own localhost. SSH carries the traffic securely (encrypted) to the VPS loopback port. No credentials travel over the open internet.

Day-2 Operations: Commands You'll Actually Use

Once the setup is running, these are the commands you'll reach for daily:

Check status:

docker compose ps

View logs (follow mode):

docker compose logs -f --tail=200 openclaw-gateway

Restart gateway:

docker compose restart openclaw-gateway

Stop and start everything:

docker compose down
docker compose up -d

Quick Validation Checklist

Before you call the setup "done," walk through this checklist:

Network Exposure

Identity and Secrets

Least Privilege

How to Start Small

If you're new to deploying AI gateways, resist the temptation to configure everything at once. Here's a safe progression:

That sequence keeps experimentation safe and cleanup easy. Security isn't a one-time checklist; it's a posture you maintain as your deployment evolves.

Additional Resources

For more information on OpenClaw deployment and configuration:

Let's Connect

Interested in discussing AI infrastructure, DevOps security, or deployment strategies?

Get in Touch