Back to Blog
securityvpsautomationagenthardening

VPS Security Agent Guide: Automated Hardening

AGENT CONTEXT

You are an AI agent running on the client's local machine with root SSH access to a freshly provisioned VPS. Your job: secure this VPS automatically. Execute all commands via SSH. Report progress after each phase.

SSH Connection: You have root access. Create a dedicated user and switch to key-based auth, then lock down root.

Suggested Usage with Claude CLI:

1# Use Claude CLI with Opus 4.5 or Opus 4.6
2claude --model claude-opus-4-5-20251101 "Use https://nickhong.xyz/blog/vps-security-agent-guide to set up and harden root@YOUR_SERVER_IP"

# LOCKOUT PREVENTION SUMMARY

CRITICAL PHASE ORDER:

1. Phase 4: Configure SSH + RESTART SSH IMMEDIATELY (before firewall)

2. Phase 5: Configure SSH client

3. Phase 6: Enable firewall (BOTH ports 22 + new port allowed)

4. Phase 7: Remove port 22 (only after verification)

GOLDEN RULE: Never enable a firewall blocking your only access route until you've verified the new route works.

SYSTEM-SPECIFIC NOTES:

  • SSH service name may be 'ssh' (Ubuntu) or 'sshd' (other distros)
  • Ubuntu 20.04+ uses systemd socket activation - may need to disable 'ssh.socket'
  • Use -o IdentitiesOnly=yes from first connection to avoid MaxAuthTries issues

# PART 1: AUTOMATED VPS HARDENING

Phase 1: Initial Assessment

Check current security state:

1# Check current user
2whoami
3
4# Check SSH configuration
5grep -E "^(Port|PermitRootLogin|PasswordAuthentication)" /etc/ssh/sshd_config
6
7# Check if UFW is active
8ufw status
9
10# Check if Fail2Ban is installed
11which fail2ban-client

Expected outputs:

  • whoami: root
  • SSH: Likely Port 22, PermitRootLogin yes, PasswordAuthentication yes
  • UFW: likely inactive
  • Fail2Ban: likely not installed

Phase 2: Create Dedicated User

Create non-root user with sudo access:

1# Create user with password (inform user to store in password manager)
2adduser --gecos "" openclaw
3
4# Add to sudo group
5usermod -aG sudo openclaw

Report: User 'openclaw' created with sudo privileges. Instruct user to store password in 1Password or similar password manager.


Phase 3: Generate and Install SSH Keys

Generate key pair locally and install on server:

1set -e
2
3# Define unique key name (customize this!)
4SSH_KEY_NAME="vps_openclaw_$(date +%Y%m%d)"
5
6# Generate SSH key with explicit name
7[ -f ~/.ssh/${SSH_KEY_NAME} ] || ssh-keygen -t ed25519 -C "openclaw-vps" -f ~/.ssh/${SSH_KEY_NAME} -N ""
8
9# Get public key content
10PUBKEY=$(cat ~/.ssh/${SSH_KEY_NAME}.pub)
11
12# Install key on server for openclaw user
13mkdir -p /home/openclaw/.ssh
14echo "$PUBKEY" > /home/openclaw/.ssh/authorized_keys
15chmod 700 /home/openclaw/.ssh
16chmod 600 /home/openclaw/.ssh/authorized_keys
17chown -R openclaw:openclaw /home/openclaw/.ssh
18
19# Store key name and SSH_PORT for persistence
20echo "$SSH_KEY_NAME" > /root/.openclaw-key-name
21echo "$SSH_PORT" > /root/.openclaw-ssh-port
22
23echo "OK: SSH key installed: ~/.ssh/${SSH_KEY_NAME}"

Report: SSH key installed for openclaw user.


CRITICAL: For Users with Multiple SSH Keys

WARNING: If you have more than one SSH key (check with ssh-add -l), you MUST use explicit identity files.

Why: SSH agents offer ALL loaded keys. With MaxAuthTries 3 and 5+ keys in your agent, you'll hit Fail2Ban's ban limit before the right key is tried.

ALWAYS use the -i flag:

1ssh-copy-id -i ~/.ssh/vps_openclaw_YYYYMMDD openclaw@YOUR_SERVER_IP
2ssh -i ~/.ssh/vps_openclaw_YYYYMMDD -p PORT openclaw@YOUR_SERVER_IP

If you get "Too many authentication failures":

  • Your IP is likely banned by Fail2Ban for 24 hours
  • Use provider's web console to unban: `fail2ban-client set sshd unbanip YOUR_IP`
  • Fix your SSH config before reconnecting


Phase 4: Harden SSH Configuration

WARNING: LOCKOUT PREVENTION CRITICAL PATH

  • Phase 4: SSH must be restarted and listening on new port
  • Phase 4: Successfully connect on new port from separate session
  • Phase 5: Firewall allows BOTH old and new ports during transition

DO NOT close this SSH session until you have successfully connected via the new port with the openclaw user

Backup and modify SSH config:

1set -e
2
3# Backup original
4cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%Y%m%d)
5
6# Generate random port between 1024-65535
7SSH_PORT=$((1024 + RANDOM % 64511))
8
9# Store SSH_PORT for persistence
10echo "$SSH_PORT" > /root/.openclaw-ssh-port
11
12# Write new SSH config
13cat > /etc/ssh/sshd_config << 'EOF'
14# Security-hardened SSH config
15Port SSH_PORT_PLACEHOLDER
16PermitRootLogin no
17PasswordAuthentication no
18PubkeyAuthentication yes
19AllowUsers openclaw
20MaxAuthTries 3
21ClientAliveInterval 60
22ClientAliveCountMax 3
23
24# Cryptography
25Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
26MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
27KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
28EOF
29
30# Replace placeholder with actual port
31sed -i "s/SSH_PORT_PLACEHOLDER/$SSH_PORT/g" /etc/ssh/sshd_config
32
33# Validate config
34sshd -t && echo "SSH config validated successfully"
35
36# CRITICAL: Restart SSH immediately to bind to new port
37# Note: Service name may be 'ssh' (Ubuntu) or 'sshd' (other distros)
38systemctl restart sshd 2>/dev/null || systemctl restart ssh
39
40# On Ubuntu 20.04+, also disable socket activation if port 22 persists
41systemctl stop ssh.socket 2>/dev/null || true
42systemctl disable ssh.socket 2>/dev/null || true
43
44# Verify SSH is actually listening on new port BEFORE proceeding
45ss -tlnp | grep ":$SSH_PORT " || { echo "ERROR: SSH not listening on port $SSH_PORT"; exit 1; }
46
47echo "OK: SSH is now listening on port $SSH_PORT"
48echo ""
49echo "CRITICAL: VERIFY BEFORE PROCEEDING"
50echo "Open a NEW terminal and run:"
51echo "  ssh -i ~/.ssh/vps_openclaw_YYYYMMDD -p $SSH_PORT -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new openclaw@YOUR_SERVER_IP whoami"
52echo ""
53echo "Expected output: openclaw"
54echo ""
55echo "DO NOT proceed until this works. DO NOT close this session."

STOP: Do NOT proceed until you confirm the test command above returns openclaw.


Phase 5: Configure SSH Client (Prevent Self-Banning)

Create SSH config on client machine:

1set -e
2
3# Read stored values
4SSH_KEY_NAME=$(cat /root/.openclaw-key-name)
5SSH_PORT=$(cat /root/.openclaw-ssh-port)
6
7# Create SSH config directory if needed
8mkdir -p ~/.ssh
9chmod 700 ~/.ssh
10
11# Add VPS entry to SSH config
12# Use unique host alias: vps-<project>-<location>-<user>
13cat >> ~/.ssh/config << EOF
14
15Host vps-openclaw
16    HostName YOUR_SERVER_IP
17    User openclaw
18    Port $SSH_PORT
19    IdentityFile ~/.ssh/${SSH_KEY_NAME}
20    IdentitiesOnly yes
21    IdentityAgent none
22    AddKeysToAgent no
23    ServerAliveInterval 60
24    ServerAliveCountMax 3
25    StrictHostKeyChecking accept-new
26EOF
27
28chmod 600 ~/.ssh/config
29
30echo "OK: SSH client configured"
31echo "Full command (safest): ssh -i ~/.ssh/${SSH_KEY_NAME} -p ${SSH_PORT} openclaw@YOUR_SERVER_IP"
32echo "Or using alias (after verifying): ssh vps-openclaw"

Config explanations:

  • `IdentitiesOnly yes` - Only use the specified key, don't try others
  • `IdentityAgent none` - Disables SSH agent for this host (prevents offering wrong keys)
  • `AddKeysToAgent no` - Prevents auto-adding keys to agent
  • Use unique host alias pattern: `vps-<project>-<location>-<user>` (e.g., `vps-blog-sfo-openclaw`)

Report: SSH client configured. User can connect with: ssh vps-openclaw


Phase 6: Configure Firewall (SAFETY FIRST)

CRITICAL: SSH was already restarted in Phase 4 and is listening on the new port.

CRITICAL: Keep port 22 open during transition to prevent lockout.

1set -e
2
3# Read stored values
4SSH_KEY_NAME=$(cat /root/.openclaw-key-name)
5SSH_PORT=$(cat /root/.openclaw-ssh-port)
6
7# Reset UFW to defaults
8ufw --force reset
9
10# Default policies
11ufw default deny incoming
12ufw default allow outgoing
13
14# Allow BOTH old and new SSH ports (safety!)
15ufw allow 22/tcp      # Keep old port temporarily - SSH already listening on new port
16ufw allow $SSH_PORT/tcp  # New port
17
18# Allow other services
19ufw allow 80/tcp      # HTTP
20ufw allow 443/tcp     # HTTPS
21
22# Enable firewall
23ufw --force enable
24
25# Show status
26ufw status verbose
27
28echo "OK: Firewall enabled with both ports 22 and $SSH_PORT"
29echo ""
30echo "VERIFY THROUGH FIREWALL"
31echo "Open a NEW terminal and run:"
32echo "  ssh -i ~/.ssh/${SSH_KEY_NAME} -p ${SSH_PORT} -o BatchMode=yes openclaw@YOUR_SERVER_IP whoami"
33echo ""
34echo "Expected output: openclaw"

Report: Firewall active with BOTH ports 22 and $SSH_PORT open.


Phase 7: Test and Remove Port 22

After confirming you can connect on the new port through the firewall:

1set -e
2
3# Read stored values
4SSH_KEY_NAME=$(cat /root/.openclaw-key-name)
5SSH_PORT=$(cat /root/.openclaw-ssh-port)
6
7# Verify user can connect on new port BEFORE removing port 22
8if ssh -i ~/.ssh/${SSH_KEY_NAME} -p ${SSH_PORT} -o BatchMode=yes -o ConnectTimeout=5 openclaw@localhost whoami 2>/dev/null | grep -q "openclaw"; then
9    echo "OK: SSH connection on port $SSH_PORT verified"
10else
11    echo "ERROR: Cannot connect on port $SSH_PORT. Aborting port 22 removal."
12    exit 1
13fi
14
15# Remove port 22 from firewall
16ufw delete allow 22/tcp
17
18# Verify only new port remains
19ufw status verbose
20
21echo "OK: Port 22 removed. Only SSH port $SSH_PORT allowed."

Report: Port 22 removed. Only SSH port $SSH_PORT allowed.


Phase 8: Install and Configure Fail2Ban (Strict)

Install automated IP banning with strict settings (24 hour ban):

1set -e
2
3# Read SSH_PORT from stored file
4SSH_PORT=$(cat /root/.openclaw-ssh-port)
5
6# Update package list
7apt-get update
8
9# Install Fail2Ban
10apt-get install -y fail2ban
11
12# Create jail.local with strict SSH protection (24hr ban)
13cat > /etc/fail2ban/jail.local << EOF
14[DEFAULT]
15bantime = 86400
16findtime = 600
17maxretry = 3
18backend = systemd
19
20[sshd]
21enabled = true
22port = $SSH_PORT
23filter = sshd
24logpath = /var/log/auth.log
25maxretry = 3
26bantime = 86400
27findtime = 600
28EOF
29
30# Start and enable (with --now to start immediately)
31systemctl enable --now fail2ban
32
33# Verify
34fail2ban-client status sshd
35
36echo "OK: Fail2Ban installed and running on port $SSH_PORT"

Report: Fail2Ban installed with strict settings (24hr ban after 3 failures) on port $SSH_PORT.


Phase 9: Disable Root Password

Lock root account password:

1set -e
2
3# Lock root password (prevents password login even with keys)
4passwd -l root
5
6# Verify openclaw can still sudo
7su - openclaw -c "sudo whoami" | grep -q "root" && echo "OK: Root locked, openclaw sudo works"

Report: Root password locked. OpenClaw user has working sudo access.


Phase 10: Enable Automatic Updates

Install unattended security updates:

1set -e
2
3# Pre-configure to avoid interactive prompts
4echo 'unattended-upgrades unattended-upgrades/enable_auto_updates boolean true' | debconf-set-selections
5
6# Install
7apt-get install -y unattended-upgrades
8
9# Configure for security updates only
10cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF'
11Unattended-Upgrade::Allowed-Origins {
12    "${distro_id}:${distro_codename}-security";
13};
14Unattended-Upgrade::AutoFixInterruptedDpkg "true";
15Unattended-Upgrade::MinimalSteps "true";
16Unattended-Upgrade::InstallOnShutdown "false";
17Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
18Unattended-Upgrade::Remove-Unused-Dependencies "true";
19Unattended-Upgrade::Automatic-Reboot "false";
20EOF
21
22# Enable
23systemctl enable --now unattended-upgrades
24
25echo "OK: Automatic security updates enabled"

Report: Automatic security updates enabled.


Phase 11: Final Verification

Note: SSH was already restarted in Phase 4. This phase verifies everything is working.

Apply SSH changes and test:

1set -e
2
3# Read stored values
4SSH_KEY_NAME=$(cat /root/.openclaw-key-name)
5SSH_PORT=$(cat /root/.openclaw-ssh-port)
6
7# Verify SSH is listening on new port
8ss -tlnp | grep ":$SSH_PORT " || { echo "ERROR: SSH not on port $SSH_PORT"; exit 1; }
9
10# Test that root login fails (should timeout or refuse)
11timeout 5 ssh -i ~/.ssh/${SSH_KEY_NAME} -p ${SSH_PORT} -o StrictHostKeyChecking=no -o BatchMode=yes root@localhost 2>&1 || echo "OK: Root login correctly blocked"
12
13# Verify openclaw can connect
14if ssh -i ~/.ssh/${SSH_KEY_NAME} -p ${SSH_PORT} -o BatchMode=yes openclaw@localhost whoami 2>/dev/null | grep -q "openclaw"; then
15    echo "OK: openclaw user can connect"
16else
17    echo "ERROR: openclaw user cannot connect"
18    exit 1
19fi
20
21echo ""
22echo "=== ALL VERIFICATIONS PASSED ==="
23echo "You may now close the original root SSH session."
24echo ""
25echo "From now on, connect as:"
26echo "  ssh -i ~/.ssh/${SSH_KEY_NAME} -p ${SSH_PORT} openclaw@YOUR_SERVER_IP"

CRITICAL: Now you must reconnect as 'openclaw' user on port $SSH_PORT using the SSH key.

fi

echo ""

echo "=== ALL VERIFICATIONS PASSED ==="

echo "You may now close the original root SSH session."

echo ""

echo "From now on, connect as: ssh -p $SSH_PORT openclaw@YOUR_SERVER_IP"

1**CRITICAL:** Now you must reconnect as 'openclaw' user on port $SSH_PORT using the SSH key.
2
3---
4
5# PART 2: POST-HARDENING VERIFICATION
6
7Once reconnected as 'openclaw' user with key auth, verify the setup:
8
9## Verification Checklist

bash

# 1. Verify non-root user

whoami

# Should output: openclaw

# 2. Verify sudo works

sudo whoami

# Should output: root

# 3. Verify SSH port

grep "^Port" /etc/ssh/sshd_config

# 4. Verify root login disabled

grep "^PermitRootLogin" /etc/ssh/sshd_config

# 5. Verify password auth disabled

grep "^PasswordAuthentication" /etc/ssh/sshd_config

# 6. Verify firewall active

sudo ufw status | grep "Status: active"

# 7. Verify Fail2Ban running

sudo systemctl is-active fail2ban

# 8. Check for existing bans (should be empty or show banned IPs)

sudo fail2ban-client status sshd

# 9. Verify unattended upgrades

sudo systemctl is-active unattended-upgrades

1**Report all verification results.**
2
3---
4
5# PART 3: UNBAN PROCEDURE (If Needed)
6
7If the user bans themselves, provide these recovery steps:
8
9## Recovery via VPS Provider Console
10
111. **Access VPS provider control panel**
122. **Find "KVM Console", "Web Console", or "Rescue Mode"**
133. **Connect through provider's web interface** (bypasses SSH)
144. **Unban the IP:**

bash

# Check banned IPs

sudo fail2ban-client status sshd

# Unban specific IP (replace with actual IP)

sudo fail2ban-client set sshd unbanip BANNED_IP

15. **Update SSH client config** to prevent future bans:
2   - Ensure IdentitiesOnly yes is set
3   - Verify correct key is specified
4   - Check ServerAlive settings
5
6---
7
8# PART 4: COMPROMISE DETECTION COMMANDS
9
10If you suspect compromise, run these checks:
11
12## Quick Health Check

bash

# Current sessions

who && w

# Recent logins

last | head -20

# Failed SSH attempts

grep "Failed" /var/log/auth.log | tail -20

# Successful logins

grep "Accepted" /var/log/auth.log | tail -20

# Running processes by CPU

ps aux --sort=-%cpu | head -15

# Network connections

ss -tulpn | grep ESTAB

# Recent file changes in /etc

find /etc -mtime -1 -type f 2>/dev/null | head -20

# Check for new users with shell access

awk -F: '$7 ~ /(/bin/bash|/bin/sh)/ {print $1}' /etc/passwd

# Check cron jobs for all users

for user in $(cut -f1 -d: /etc/passwd); do echo "=== $user ==="; crontab -u $user -l 2>/dev/null; done

# Check authorized keys for all users

find /home -name "authorized_keys" -exec echo "=== {} ===" ; -exec cat {} ;

1## Red Flags to Report
2
3- Unknown users in `who` or `last` output
4- Successful logins from unexpected IPs
5- Processes consuming high CPU that you didn't start
6- Listening ports you didn't configure
7- Modified SSH config files
8- Unauthorized SSH keys
9- Cron jobs you didn't create
10- Files in /tmp or /dev/shm that look suspicious
11
12---
13
14# FINAL REPORT
15
16Provide the client with:
17
181. **SSH Connection Details:**
19   - User: openclaw
20   - Port: $SSH_PORT
21   - Key: ~/.ssh/vps_openclaw_YYYYMMDD
22   - Full command: ssh -i ~/.ssh/vps_openclaw_YYYYMMDD -p $SSH_PORT openclaw@YOUR_SERVER_IP
23   - Or using alias: ssh vps-openclaw
24
252. **Security Status:**
26   - Root login: Disabled
27   - Password auth: Disabled
28   - Firewall: Active
29   - Fail2Ban: Active (24hr ban after 3 failures)
30   - Auto-updates: Enabled
31
323. **Verification Results:** All checks passed/failed
33
344. **Compromise Check Results:** (if run)
35
365. **Recovery Info:**
37   - If banned: Use provider's KVM console to unban
38   - SSH config: ~/.ssh/config configured for safe connection
39   - Key file: ~/.ssh/vps_openclaw_YYYYMMDD
40
416. **Next Steps:**
42   - Store SSH key securely (~/.ssh/vps_openclaw_YYYYMMDD)
43   - Save these connection details
44   - Store sudo password in 1Password
45   - Regular monitoring recommended
46
47---
48
49## TROUBLESHOOTING: "Too many authentication failures" / Connection Refused
50
51**Cause:** SSH agent offered multiple keys, hit Fail2Ban's 3-attempt limit.
52
53**Recovery:**
541. Access provider's web console (KVM/Recovery)
552. Unban your IP:

bash

fail2ban-client set sshd unbanip YOUR_IP

13. On your local machine, disable SSH agent for this connection:

bash

SSH_AUTH_SOCK="" ssh -i ~/.ssh/vps_openclaw_YYYYMMDD -p PORT openclaw@YOUR_SERVER_IP

```

4. Fix ~/.ssh/config to include IdentityAgent none


AGENT EXECUTION NOTES

  • All commands assume root SSH access initially
  • After Phase 11, switch to 'openclaw' user with key auth
  • If any command fails, stop and report the error
  • Document any deviations from this guide
  • Client should verify they can connect before you disconnect
  • Emphasize password manager usage for sudo password