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