This cheatsheet walks through hardening a fresh Ubuntu 24.04 VPS from first root login to a fully isolated Docker-hosted OpenClaw gateway — accessible only over an SSH tunnel from your your System.

Before you start: You need a working SSH client on macOS, a YOUR_VPS_PROVIDER (or equivalent) VPS with Ubuntu 24.04, and an Anthropic API key. Generate a dedicated SSH key pair first:


ssh-keygen -t ed25519 -C "vps-YOUR_VPS_PROVIDER" -f ~/.ssh/id_ed25519_vps



Step 1: Initial Root Access & System Update


Connect as root and fully upgrade the system before touching anything else. Never configure a stale base.


ssh root@YOUR_VPS_IP

apt update && apt full-upgrade -y
apt autoremove -y
reboot



After reboot, reconnect and set your timezone:


ssh root@YOUR_VPS_IP

timedatectl set-timezone YOUR_TIMEZONE
timedatectl status



Step 2: Create an Admin User


Create a personal admin account with sudo. You will manage the server as this user — never as root, and never as the app user.


# On the VPS (as root)
adduser YOUR_USER
usermod -aG sudo YOUR_USER

# From your your System — copy your SSH public key to the new user
ssh-copy-id -i ~/.ssh/id_ed25519_vps.pub YOUR_USER@YOUR_VPS_IP



Open a new terminal tab and test the login before closing the root session:


ssh YOUR_USER@YOUR_VPS_IP
sudo whoami   # expected output: root



Step 3: Harden SSH


Disable root login, enforce key-only auth, and move SSH to a non-default port. Validate the config before restarting — a typo will lock you out.


# Back in the root session
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
nano /etc/ssh/sshd_config



Set or add these directives (adjust port if needed):


Port NEW_PORT
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2



Validate and restart SSH — open the firewall port first (Step 4) if you change the port:


sshd -t   # must produce no output
systemctl daemon-reload
systemctl restart ssh.socket
systemctl restart ssh

# From your your System — confirm login on the new port
ssh -p NEW_PORT YOUR_USER@YOUR_VPS_IP



Step 4: Configure UFW Firewall


Deny all incoming by default, then allow only SSH. Docker will be handled separately — it bypasses UFW's INPUT chain and requires its own mitigation.


sudo apt install ufw -y

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow NEW_PORT/tcp comment "SSH"

sudo ufw enable
sudo ufw status verbose



Block Docker from silently exposing container ports to the public internet by adding a DOCKER-USER chain rule:


sudo nano /etc/ufw/after.rules



Append this block at the very end of the file:


# DOCKER-USER — block unexpected container port exposure
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
-A DOCKER-USER -s 127.0.0.0/8 -j RETURN
-A DOCKER-USER -s 10.0.0.0/8 -j RETURN
-A DOCKER-USER -s 172.16.0.0/12 -j RETURN
-A DOCKER-USER -s 192.168.0.0/16 -j RETURN
-A DOCKER-USER -s 100.64.0.0/10 -j RETURN
-A DOCKER-USER -p tcp --dport 80 -j RETURN
-A DOCKER-USER -p tcp --dport 443 -j RETURN
-A DOCKER-USER -m conntrack --ctstate NEW -j DROP
-A DOCKER-USER -j RETURN
COMMIT



sudo ufw reload
# After Docker is installed, verify the chain exists:
sudo iptables -S DOCKER-USER



Step 5: Install Fail2Ban


Fail2Ban reads systemd journal logs and bans IPs that fail authentication. Configure it to watch the SSH jail on your custom port.


sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local



Find the [DEFAULT] section and the [sshd] section and set:


[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

[sshd]
enabled  = true
port     = NEW_PORT
logpath  = %(sshd_log)s
maxretry = 3
bantime  = 24h



sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status sshd



Step 6: System Monitoring (auditd, AIDE, logwatch)


Install auditd for syscall auditing, AIDE for file integrity checks, and logwatch for daily log digests.


# auditd
sudo apt install auditd audispd-plugins -y
sudo systemctl enable auditd
sudo systemctl start auditd

sudo nano /etc/audit/rules.d/hardening.rules



Paste these audit rules:


-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/ssh/sshd_config -p wa -k sshd_config
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands
-w /var/run/docker.sock -p rwxa -k docker_socket
-w /home/openclaw/.openclaw -p wa -k openclaw_state



sudo augenrules --load
sudo auditctl -l   # verify rules loaded



# AIDE — file integrity baseline
sudo apt install aide -y
sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Schedule nightly check at 03:00
echo "0 3 * * * root /usr/bin/aide --check >> /var/log/aide.log 2>&1" | sudo tee /etc/cron.d/aide-check



# logwatch — daily digest
sudo apt install logwatch -y
sudo nano /etc/logwatch/conf/logwatch.conf
# Set: Output = mail | MailTo = your@email.com | Detail = Med

# Automatic security patches
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades



Step 7: Create the OpenClaw App User (no sudo)


The openclaw user owns the config and state directories. It has no sudo, no Docker group membership, and is never accessed via SSH directly — only by switching from the admin user.


sudo adduser openclaw --disabled-password --gecos ""

sudo mkdir -p /home/openclaw/.openclaw
sudo chown -R openclaw:openclaw /home/openclaw/.openclaw
sudo chmod 700 /home/openclaw/.openclaw



Step 8: Install Docker


Install from Docker's official repository. Do not add the openclaw user to the docker group — Docker group membership is equivalent to root access.


sudo apt remove docker docker-engine docker.io containerd runc 2>/dev/null

sudo apt install ca-certificates curl gnupg -y
sudo install -m 0755 -d /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y

sudo docker run --rm hello-world



Step 9: Configure OpenClaw Secrets and Workspace


Switch to the openclaw user, create the directories that will be bind-mounted into the container, and write your API keys into a tightly-permissioned .env file.


sudo su - openclaw

mkdir -p ~/.openclaw/workspace
chmod 700 ~/.openclaw
chmod 700 ~/.openclaw/workspace

nano ~/.openclaw/.env



ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxx
# Generate with: openssl rand -hex 32
OPENCLAW_GATEWAY_TOKEN=your-64-char-random-token-here



chmod 600 ~/.openclaw/.env
exit   # back to admin user



# Clone the repo as admin, set ownership to openclaw
sudo -u openclaw git clone https://github.com/openclaw/openclaw.git /home/openclaw/openclaw-repo
sudo chown -R openclaw:openclaw /home/openclaw/openclaw-repo



Step 10: Build and Start OpenClaw in Docker


Run the setup script from the repo directory. It builds the image, runs the onboarding wizard, and starts the gateway container.


cd /home/openclaw/openclaw-repo

sudo OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest" \
     OPENCLAW_HOME_VOLUME="openclaw_home" \
     ./scripts/docker/setup.sh



If the script doesn't prompt correctly, run onboarding manually:


# Manual onboarding
sudo docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
  dist/index.js onboard --mode local --no-install-daemon

# Set bind to loopback — never expose on LAN on a VPS
sudo docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
  dist/index.js config set --batch-json \
  '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"loopback"},{"path":"gateway.auth.mode","value":"token"}]'

sudo docker compose up -d openclaw-gateway

# Verify
sudo docker compose ps
sudo docker compose logs -f openclaw-gateway



Step 11: Apply OpenClaw Security Hardening Config


Lock down the gateway config: loopback-only bind, token auth, mDNS off, filesystem restricted to workspace, exec tools set to deny-by-default.


sudo docker compose run --rm openclaw-cli security audit --deep

sudo -u openclaw nano /home/openclaw/.openclaw/openclaw.json



{
  "gateway": {
    "mode": "local",
    "bind": "loopback",
    "port": 18789,
    "auth": {
      "mode": "token",
      "token": "${OPENCLAW_GATEWAY_TOKEN}"
    }
  },
  "discovery": {
    "mdns": { "mode": "off" }
  },
  "session": {
    "dmScope": "per-channel-peer"
  },
  "tools": {
    "deny": ["gateway", "cron", "sessions_spawn", "sessions_send"],
    "fs": { "workspaceOnly": true },
    "exec": { "security": "deny", "ask": "always" }
  },
  "logging": {
    "redactSensitive": "tools"
  }
}



sudo chmod 600 /home/openclaw/.openclaw/openclaw.json
sudo docker compose restart openclaw-gateway



Step 12: Access the Dashboard via SSH Tunnel


The gateway listens only on 127.0.0.1:18789 inside the VPS. Forward that port to your your System over SSH — never expose it to the internet directly.


# On your your System — keep this terminal open while using the dashboard
ssh -p NEW_PORT -N -L 18789:127.0.0.1:18789 YOUR_USER@YOUR_VPS_IP



Then open http://127.0.0.1:18789/ in your browser and authenticate with the value of OPENCLAW_GATEWAY_TOKEN.

Add this to ~/.ssh/config on your your System for one-command access:


Host openclaw-vps
  HostName YOUR_VPS_IP
  User YOUR_USER
  Port NEW_PORT
  IdentityFile ~/.ssh/id_ed25519_vps
  LocalForward 18789 127.0.0.1:18789



ssh -N openclaw-vps



Step 13: Verify the Full Setup


Run health checks and confirm all security controls are in place before considering this server production-ready.


# Health endpoints
curl -fsS http://127.0.0.1:18789/healthz
curl -fsS http://127.0.0.1:18789/readyz

# Gateway and security audit
sudo docker compose run --rm openclaw-cli gateway status
sudo docker compose run --rm openclaw-cli security audit --deep

# Container isolation — must NOT be root, must NOT have Docker socket
sudo docker compose exec openclaw-gateway whoami          # expected: node
sudo docker compose exec openclaw-gateway ls /var/run/docker.sock 2>&1   # expected: No such file

# UFW and DOCKER-USER chain
sudo ufw status verbose
sudo iptables -S DOCKER-USER   # verify DROP rule is present

# Fail2Ban
sudo fail2ban-client status
sudo fail2ban-client status sshd



Ongoing Maintenance


Common day-to-day operations once the server is running.


# Update OpenClaw
cd /home/openclaw/openclaw-repo
sudo docker compose pull
sudo docker compose up -d

# View gateway logs
sudo docker compose logs -f openclaw-gateway

# Backup state
sudo tar -czf openclaw-backup-$(date +%Y%m%d).tar.gz \
  /home/openclaw/.openclaw/openclaw.json \
  /home/openclaw/.openclaw/credentials/ \
  /home/openclaw/.openclaw/agents/

# Rotate the gateway token
openssl rand -hex 32
sudo nano /home/openclaw/.openclaw/.env
sudo docker compose restart openclaw-gateway