VPS Security Hardening Manual

SSH Hardening, Root Access Lockdown & API Key Management

For OpenClaw / Skills-as-SaaS Deployments

February 2026

Table of Contents

1. Introduction & Threat Model

2. SSH Hardening

2.1 Generate SSH Key Pair

2.2 Copy Public Key to Server

2.3 Configure SSHD

2.4 Set Up a Firewall (UFW)

2.5 Install Fail2Ban

3. Disable Password-Based Root Access

3.1 Create a Non-Root Admin User

3.2 Lock Down Root

3.3 Verify the Lockdown

4. Treat API Keys as Production Credentials

4.1 Never Hardcode Keys

4.2 Environment Variables & .env Files

4.3 Secrets Manager Options

4.4 Key Rotation Strategy

4.5 Monitoring & Auditing

5. Quick-Reference Checklists

6. Emergency Response Playbook

1. Introduction & Threat Model

When you deploy a VPS to host an OpenClaw backend service, a trading bot, or any skill-as-SaaS application, you are placing a publicly reachable computer on the internet. Automated bots will begin probing your server within minutes of it going live. This manual covers the three most critical layers of defense you need from day one.

⚠ WARNING: A misconfigured VPS can expose your API keys, customer data, and the server itself to attackers. Do not skip any section.

Threat Landscape

Threat How It Happens What You Lose
Brute-force SSH Bots try thousands of password combos Full server control
Leaked API keys Keys committed to Git or left in code Billing fraud, data theft
Root compromise Attacker gains root via weak password Total system takeover
Privilege escalation Non-root exploit chains to root Lateral movement, data exfil

2. SSH Hardening

SSH is the front door to your server. By default, most VPS providers ship with password authentication enabled and root login allowed. This section locks that down.

2.1 Generate an SSH Key Pair (On Your Local Machine)

SSH keys use public-key cryptography. The private key stays on your machine; the public key goes on the server. No password ever crosses the network.

On macOS / Linux:

ssh-keygen -t ed25519 -C "your_email@example.com"

# When prompted:

# File: press Enter for default (~/.ssh/id_ed25519)

# Passphrase: enter a STRONG passphrase (adds 2nd factor)

On Windows (PowerShell):

ssh-keygen -t ed25519 -C "your_email@example.com"

# Keys saved to C:\Users\YourName\.ssh\id_ed25519

✅ TIP: Always use ed25519 over RSA. It is faster, more secure, and produces shorter keys.

What this creates:

File Purpose Share It?
id_ed25519 Private key (YOUR SECRET) NEVER
id_ed25519.pub Public key Yes – goes on server

2.2 Copy Your Public Key to the Server

Method A – ssh-copy-id (easiest):

ssh-copy-id -i ~/.ssh/id_ed25519.pub root@YOUR_SERVER_IP

Method B – Manual (if ssh-copy-id unavailable):

# On your local machine, display the public key:

cat ~/.ssh/id_ed25519.pub

# SSH into the server and paste it:

ssh root@YOUR_SERVER_IP

mkdir -p ~/.ssh && chmod 700 ~/.ssh

echo 'PASTE_YOUR_PUBLIC_KEY_HERE' >> ~/.ssh/authorized_keys

chmod 600 ~/.ssh/authorized_keys

Test the key before proceeding:

ssh -i ~/.ssh/id_ed25519 root@YOUR_SERVER_IP

# Should log in WITHOUT asking for a password

2.3 Configure the SSH Daemon (sshd_config)

Now that key-based login works, disable everything else.

Edit the config file:

sudo nano /etc/ssh/sshd_config

Find and change these lines (or add them if missing):

# --- CRITICAL SETTINGS ---

# Disable password authentication entirely

PasswordAuthentication no

# Disable root login (we will create a separate admin user)

PermitRootLogin no

# Disable challenge-response (another password vector)

ChallengeResponseAuthentication no

KbdInteractiveAuthentication no

# Only allow SSH protocol 2

Protocol 2

# Limit login attempts per connection

MaxAuthTries 3

# Disconnect idle sessions after 5 minutes

ClientAliveInterval 300

ClientAliveCountMax 0

# Disable X11 forwarding (not needed for servers)

X11Forwarding no

# Disable empty passwords

PermitEmptyPasswords no

# --- OPTIONAL: Change default port ---

# Port 2222 # Reduces noise from automated bots

⚠ IMPORTANT: Keep your current SSH session open until you verify the new config works in a SECOND terminal window.

Restart SSH and test:

sudo systemctl restart sshd

# In a NEW terminal (keep old one open!):

ssh -i ~/.ssh/id_ed25519 your_admin_user@YOUR_SERVER_IP

2.4 Set Up a Firewall (UFW)

UFW (Uncomplicated Firewall) blocks all ports except the ones you explicitly allow.

# Allow SSH (change 22 to your custom port if applicable)

sudo ufw allow 22/tcp

# Allow your app port (e.g., OpenClaw backend on 3000)

sudo ufw allow 3000/tcp

# Allow HTTPS if you run a landing page

sudo ufw allow 443/tcp

# Enable the firewall

sudo ufw enable

# Verify

sudo ufw status verbose

2.5 Install Fail2Ban

Fail2Ban monitors log files and automatically bans IPs that show malicious signs (like repeated failed SSH logins).

sudo apt update && sudo apt install fail2ban -y

# Create local config (never edit the main file)

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

sudo nano /etc/fail2ban/jail.local

Key settings to adjust in jail.local:

[sshd]

enabled = true

port = 22 # Match your SSH port

maxretry = 3 # Ban after 3 failed attempts

bantime = 3600 # Ban for 1 hour (seconds)

findtime = 600 # Within 10-minute window

sudo systemctl enable fail2ban

sudo systemctl start fail2ban

# Check status

sudo fail2ban-client status sshd

3. Disable Password-Based Root Access

The root user has unrestricted access to everything on the server. If an attacker gets root, they own the machine. The strategy here is simple: never log in as root. Instead, create a dedicated admin user with sudo privileges.

3.1 Create a Non-Root Admin User

# While still logged in as root:

adduser deployer

# Set a strong password when prompted

# Grant sudo privileges

usermod -aG sudo deployer

# Copy your SSH key to the new user

mkdir -p /home/deployer/.ssh

cp ~/.ssh/authorized_keys /home/deployer/.ssh/

chown -R deployer:deployer /home/deployer/.ssh

chmod 700 /home/deployer/.ssh

chmod 600 /home/deployer/.ssh/authorized_keys

Test login as the new user (in a new terminal):

ssh -i ~/.ssh/id_ed25519 deployer@YOUR_SERVER_IP

# Verify sudo works:

sudo whoami

# Should output: root

3.2 Lock Down Root

Step A: Disable root SSH login

You already set this in sshd_config above, but verify it:

sudo grep PermitRootLogin /etc/ssh/sshd_config

# Must show: PermitRootLogin no

Step B: Lock the root password

# This prevents ANY password-based root login, even on console

sudo passwd -l root

Step C: Restrict su to admin group only

sudo dpkg-statoverride --update --add root sudo 4750 /bin/su

3.3 Verify the Lockdown

Run these checks to confirm everything is properly locked down:

Test Expected Result
ssh root@SERVER_IP Permission denied (publickey)
ssh -o PubkeyAuth=no deployer@SERVER_IP Permission denied (publickey)
sudo passwd -S root root L ... (L means locked)
sudo fail2ban-client status sshd Shows active jail with ban count

4. Treat API Keys as Production Credentials

API keys for OpenAI, Anthropic, OANDA, Stripe, and any other service are as sensitive as passwords. A leaked key can drain your account balance, expose customer data, or allow attackers to impersonate your service.

⚠ CRITICAL: If you accidentally push an API key to a public GitHub repo, consider it compromised IMMEDIATELY. Rotate the key, then clean the Git history.

4.1 Never Hardcode Keys

The single most common mistake. Never put API keys directly in your source code.

❌ WRONG (Hardcoded) ✅ RIGHT (Environment Var)
api_key = "sk-abc123..." api_key = os.environ["OPENAI_KEY"]

4.2 Environment Variables & .env Files

Option A: System environment variables (simplest for VPS)

# Add to /home/deployer/.bashrc or .profile:

export OPENAI_API_KEY="sk-your-key-here"

export OANDA_API_KEY="your-oanda-key"

export STRIPE_SECRET_KEY="sk_live_your-stripe-key"

# Reload:

source ~/.bashrc

# In your app (Python):

import os

api_key = os.environ["OPENAI_API_KEY"]

# In your app (Node.js):

const apiKey = process.env.OPENAI_API_KEY;

Option B: .env file with dotenv library

# Create .env in your project root:

OPENAI_API_KEY=sk-your-key-here

OANDA_API_KEY=your-oanda-key

STRIPE_SECRET_KEY=sk_live_your-stripe-key

DATABASE_URL=postgresql://user:pass@localhost/mydb

⚠ CRITICAL: Add .env to your .gitignore IMMEDIATELY. Create .env.example with placeholder values for teammates.

# .gitignore - add these lines:

.env

.env.local

.env.production

*.pem

*.key

Set proper file permissions on .env:

chmod 600 .env # Only owner can read/write

chown deployer:deployer .env

4.3 Secrets Manager Options (For Growing Projects)

When you move beyond a single VPS, consider a dedicated secrets manager:

Tool Best For Cost Complexity
System env vars Single VPS Free Low
.env + dotenv Small projects Free Low
Docker secrets Containerized apps Free Medium
AWS Secrets Manager AWS deployments ~$0.40/secret/mo Medium
HashiCorp Vault Multi-env / team Free (self-host) High

4.4 Key Rotation Strategy

Rotate keys regularly, and always rotate immediately if a compromise is suspected.

# Create a simple key-rotation-log.md:

| Key | Last Rotated | Next Due | Owner |

|--------------|-------------|------------|----------|

| OPENAI_KEY | 2026-02-01 | 2026-05-01 | deployer |

| STRIPE_KEY | 2026-02-15 | 2026-03-15 | deployer |

| OANDA_KEY | 2026-01-10 | 2026-04-10 | deployer |

4.5 Monitoring & Auditing

Monitor API usage for anomalies:

Audit key access:

# Check who has read access to your .env file:

ls -la .env

# Search for any hardcoded keys in your codebase:

grep -rn "sk-" --include="*.py" --include="*.js" .

grep -rn "sk_live" --include="*.py" --include="*.js" .

# Use git-secrets to prevent committing keys:

# https://github.com/awslabs/git-secrets

git secrets --install

git secrets --register-aws

git secrets --add 'sk-[a-zA-Z0-9]{20,}' # OpenAI pattern

5. Quick-Reference Checklists

SSH Hardening Checklist

Root Access Lockdown Checklist

API Key Security Checklist

6. Emergency Response Playbook

If you suspect a security breach, act fast. Here is your step-by-step response plan.

Scenario A: Suspected SSH Compromise

Scenario B: Leaked API Key

✅ REMEMBER: Leaked keys must be treated as compromised even if you have no evidence of misuse. Always revoke and rotate.

End of Manual