- Published on
Hardening a VPS for Clawdbot: SSH, fail2ban, and Security Validation
- Authors

- Name
- Avasdream
- @avasdream_
If you're running Clawdbot on a VPS, you're putting an AI agent with shell access on a public-facing server. That's powerful — but it means security isn't optional.
This guide walks through hardening a fresh Ubuntu VPS step by step, installing and configuring Clawdbot, and validating that nothing is exposed. I'll show real attack data from my own server to demonstrate why each step matters.
Clawdbot's Architecture and Attack Surface
Before hardening, it helps to understand what you're protecting. Clawdbot runs a WebSocket gateway (default port 18789) that acts as the central hub — it connects to your messaging channels (Telegram, Discord, WhatsApp), runs the AI agent, and provides tool access.
A typical Clawdbot installation has access to:
| Capability | What It Can Do | Risk If Compromised |
|---|---|---|
| Shell execution | Run arbitrary commands as your user | Full system compromise |
| File system | Read/write workspace, configs, memory files | Data theft, config poisoning |
| Messaging | Send messages on Telegram, Discord, etc. | Identity abuse, social engineering |
| Web requests | Fetch URLs, browse pages, call APIs | Data exfiltration, SSRF |
| Cron jobs | Schedule persistent background tasks | Long-term persistence |
| Memory files | Store data that survives across sessions | Planted triggers, surveillance |
| API keys | LLM provider credentials (Anthropic, OpenAI) | Financial abuse, key theft |
This is why security matters: an attacker who gains access doesn't just read your files — they control an autonomous agent that can act on your behalf.
The good news: Clawdbot's defaults are secure. The gateway binds to 127.0.0.1 (localhost only) and is invisible from the internet. In my Shodan research scanning 200 real-world Clawdbot instances, I found:
- 96% (192/200) have the gateway port completely firewalled — unreachable from the internet
- 4% (8/200) expose the port but require challenge-response authentication
- 0% (0/200) were running without authentication
Every single instance was properly secured. But the VPS itself still needs hardening — especially SSH, which is under constant automated attack.
The Attack Surface Is Real
Within minutes of provisioning a Hetzner VPS, I checked the SSH logs:
sudo journalctl -u ssh --since "15 min ago"
15 minutes. Dozens of brute-force attempts. Here's a sample:
sshd: Invalid user admin from 178.62.250.160
sshd: Failed password for root from 167.99.220.88
sshd: Invalid user solana from 2.57.122.177
sshd: Failed password for root from 45.148.10.141
sshd: Invalid user claude from 45.78.218.101
sshd: Invalid user elasticsearch from 45.78.216.14
Bots were trying usernames like admin, root, solana, claude, elasticsearch, nagios, and gitlab — cycling through common service accounts every few seconds. Multiple IPs were attacking simultaneously.
This is what every VPS faces from the moment it goes online. Let's fix it.
Step 1: Initial Server Setup
Start with a fresh Ubuntu 22.04 or 24.04 VPS (Hetzner, DigitalOcean, AWS Lightsail — any provider works). SSH in as root and update everything:
apt update && apt upgrade -y
apt install -y git curl build-essential jq ca-certificates openssl htop
Create a Non-Root User
Don't run Clawdbot as root. Create a dedicated user:
adduser clawdbot
usermod -aG sudo clawdbot
Set a strong password. You'll switch to key-only authentication shortly, but the password is your safety net during setup.
Add Swap (For 2 GB RAM VPS)
Small instances can run out of memory during npm installs. A swap file prevents this:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl vm.swappiness=10
Step 2: SSH Hardening
SSH is the only public-facing service on a typical Clawdbot VPS. Lock it down.
Set Up Key Authentication
On your local machine, generate a key and copy it to the server:
# Generate (skip if you already have a key)
ssh-keygen -t ed25519 -C "yourname@device"
# Copy to server
ssh-copy-id clawdbot@your-server-ip
Test the key login in a separate terminal before proceeding. You need to confirm passwordless login works before disabling passwords.
Harden sshd_config
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
sudo tee /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
# Disable root login entirely
PermitRootLogin no
# Key-only authentication
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
# Limit access to your user only
AllowUsers clawdbot
MaxAuthTries 3
MaxSessions 3
LoginGraceTime 30
# Disable unnecessary features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
# Timeout idle sessions (5 min)
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
Validate and apply — keep your current session open:
sudo sshd -t # Must produce no output (= valid)
sudo systemctl reload ssh
Test in a new terminal before closing the old one. If you lock yourself out, you'll need your provider's console access.
What This Does
PermitRootLogin no— Eliminates the most targeted accountPasswordAuthentication no— Brute-force becomes impossible (keys only)AllowUsers clawdbot— Only your user can log inMaxAuthTries 3— Disconnects after 3 failed attemptsLoginGraceTime 30— 30 seconds to authenticate or get dropped
Step 3: Firewall (UFW)
UFW (Uncomplicated Firewall) is usually pre-installed on Ubuntu but inactive by default. Enable it with minimal rules:
# Default: block everything incoming, allow outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (do this FIRST or you'll lock yourself out!)
sudo ufw allow 22/tcp comment 'SSH'
# Enable the firewall
sudo ufw enable
# Verify
sudo ufw status verbose
Why No Other Ports?
Clawdbot's gateway binds to 127.0.0.1:18789 by default — it's only accessible from the server itself and doesn't need a firewall rule. Telegram uses long-polling (outbound HTTPS to api.telegram.org), which is covered by default allow outgoing. No inbound ports are needed for the gateway.
If you later set up webhooks or a reverse proxy, you'd open port 443:
sudo ufw allow 443/tcp comment 'HTTPS'
Rate-Limit SSH
For extra protection, replace the basic SSH rule with rate limiting:
sudo ufw delete allow 22/tcp
sudo ufw limit 22/tcp comment 'SSH rate limited'
This caps connections at 6 per 30 seconds per source IP — stops rapid brute-force attempts at the firewall level before fail2ban even needs to act.
Step 4: fail2ban
UFW blocks ports. fail2ban watches logs and bans IPs that show malicious behavior.
Install
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
Configure
Create a local config file — never edit jail.conf directly, it gets overwritten on updates:
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1
# Add your home IP to avoid locking yourself out:
# ignoreip = 127.0.0.1/8 ::1 YOUR.HOME.IP
# Standard SSH jail
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 3
bantime = 2h
# Aggressive mode — catches invalid users, protocol errors
[sshd-aggressive]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
filter = sshd[mode=aggressive]
maxretry = 3
bantime = 24h
# Repeat offenders get banned for 1 week
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
maxretry = 5
EOF
Apply and Verify
sudo systemctl restart fail2ban
sudo fail2ban-client status # List active jails
sudo fail2ban-client status sshd # Show SSH jail stats
Results on My Server
After installing fail2ban, the first scan immediately caught the attackers from the logs above:
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| |- Total failed: 1
`- Actions
|- Currently banned: 10
|- Total banned: 10
`- Banned IP list: 101.47.161.248 167.99.220.88 178.62.250.160
2.57.122.177 2.57.122.209 45.148.10.141
45.78.207.24 45.78.216.14 45.78.218.101
45.78.230.30
10 IPs banned instantly. These were the same IPs hammering SSH with fake usernames every few seconds. Now they're locked out.
How the Three Jails Work Together
- sshd — 3 failures in 10 minutes → 2-hour ban
- sshd-aggressive — Catches invalid users and protocol violations → 24-hour ban
- recidive — If an IP gets banned 5 times in a day → 1-week ban on all ports
This creates progressive punishment. Casual scanners get a short ban, persistent attackers get blocked for a week.
Useful Commands
# Check banned IPs
sudo fail2ban-client status sshd
# Manually unban an IP (if you lock yourself out)
sudo fail2ban-client set sshd unbanip YOUR.IP.HERE
# View fail2ban logs
sudo tail -f /var/log/fail2ban.log
Step 5: Automatic Security Updates
Don't rely on remembering to run apt upgrade. Let the system handle critical patches:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Select: YES
Verify it's configured:
cat /etc/apt/apt.conf.d/20auto-upgrades
Should show:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
This automatically installs security patches daily. You can optionally enable automatic reboots for kernel updates (recommended for unattended servers):
sudo sed -i 's|//Unattended-Upgrade::Automatic-Reboot "false"|Unattended-Upgrade::Automatic-Reboot "true"|' \
/etc/apt/apt.conf.d/50unattended-upgrades
sudo sed -i 's|//Unattended-Upgrade::Automatic-Reboot-Time "02:00"|Unattended-Upgrade::Automatic-Reboot-Time "04:00"|' \
/etc/apt/apt.conf.d/50unattended-upgrades
Step 6: Install Clawdbot
With the server hardened, switch to your non-root user and install Clawdbot.
Install Node.js 22
Clawdbot requires Node.js 22 or newer:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version # Should show v22.x
Install Clawdbot
# Option A: One-liner installer (recommended)
curl -fsSL https://clawd.bot/install.sh | bash
# Option B: Manual global install
sudo npm install -g clawdbot@latest
Onboard
Run the onboarding wizard, which sets up everything interactively:
clawdbot onboard --install-daemon
The wizard walks you through:
- Gateway mode — Local (this is what you want on a VPS)
- Authentication — API key for your LLM provider (Anthropic recommended) or OAuth
- Channel setup — Telegram bot token, Discord, WhatsApp QR, etc.
- Pairing defaults — Secure DM access (pairing mode by default)
- Workspace — Where the agent stores files (default:
~/clawd) - Background service — Installs as a systemd user service
Enable Persistent Service
Make sure the service stays running after you disconnect from SSH:
sudo loginctl enable-linger "$USER"
systemctl --user daemon-reload
systemctl --user enable --now clawdbot-gateway.service
systemctl --user status clawdbot-gateway.service
Verify the Install
clawdbot status # Gateway info + sessions
clawdbot health # Connectivity check
clawdbot doctor # Full diagnostic
Step 7: Clawdbot Configuration
After onboarding, you'll want to review and tighten the configuration. The config lives at ~/.clawdbot/clawdbot.json (JSON5 — comments and trailing commas are allowed).
Minimal Secure Config
Here's a recommended starting point for a Telegram-based setup:
{
// Agent settings
agents: {
defaults: {
workspace: "~/clawd",
}
},
// Telegram channel
channels: {
telegram: {
enabled: true,
botToken: "YOUR_BOT_TOKEN",
// Only allow your Telegram user ID
dmPolicy: "pairing",
// Groups: require @mention to respond
groups: {
"*": { requireMention: true }
}
}
}
}
Key Configuration Options
DM access control — dmPolicy determines who can talk to your bot:
"pairing"(default, recommended) — Unknown senders get a pairing code. You approve them viaclawdbot pairing approve telegram <CODE>. Messages are not processed until approved."allowlist"— Only pre-approved user IDs can message the bot."open"— Anyone can message (not recommended unless you know what you're doing).
Group behavior — Control how the bot behaves in group chats:
requireMention: true— Bot only responds when @mentioned (recommended)- Group allowlists restrict which groups the bot participates in
Gateway auth — The onboarding wizard generates a gateway token by default. If you ever need to bind beyond localhost (not recommended), always set auth:
{
gateway: {
auth: {
mode: "password",
password: "a-strong-random-password"
}
}
}
First Contact: Pairing
When you first message your bot on Telegram, it will respond with a pairing code instead of processing your message. Approve it from the server:
clawdbot pairing list telegram
clawdbot pairing approve telegram ABCD1234
After approval, your Telegram user ID is added to the allowlist and the bot starts responding normally.
Testing the Setup
Send a simple message to your bot on Telegram. Then verify from the server:
# Check active sessions
clawdbot status
# View recent logs
journalctl --user -u clawdbot-gateway.service -n 50
# Run a full security check
clawdbot security audit --deep
The security audit command checks gateway exposure, DM/group policies, file permissions, browser control, and plugin trust. Run it with --fix to auto-apply safe defaults:
clawdbot security audit --fix
Step 8: Validate with the Self-Audit Script
I wrote a self-audit script that checks whether your Clawdbot instance is properly secured. It verifies gateway binding, external port exposure, mDNS, authentication, file permissions, and firewall status.
Run it directly:
curl -sL https://gist.githubusercontent.com/AvasDream/020ca16d72226213aec94a3a7f2844e6/raw/self-audit.sh | bash
Or download and review first:
wget https://gist.githubusercontent.com/AvasDream/020ca16d72226213aec94a3a7f2844e6/raw/self-audit.sh
cat self-audit.sh
bash self-audit.sh
Expected Output
On a properly hardened VPS, you should see all checks passing:
╔══════════════════════════════════════════════════════════════╗
║ CLAWDBOT SECURITY SELF-AUDIT ║
╚══════════════════════════════════════════════════════════════╝
📍 External IP: 203.0.113.10
1️⃣ GATEWAY BINDING
✅ Gateway bound to localhost only (127.0.0.1:18789)
2️⃣ EXTERNAL PORT ACCESS
✅ Port 18789 is CLOSED externally
3️⃣ mDNS EXPOSURE
✅ Avahi/mDNS not running
4️⃣ AUTHENTICATION
✅ Gateway token configured (64 chars)
5️⃣ FILE PERMISSIONS
✅ Config file permissions: 600
6️⃣ FIREWALL
✅ UFW active, port 18789 not explicitly allowed
SUMMARY: ✅ 6 passed | ⚠️ 0 warnings | ❌ 0 failed
🟢 All checks passed! Your Clawdbot instance is well-secured.
If any check fails, the script tells you what to fix. The most critical checks:
- Gateway binding — Must be
127.0.0.1, not0.0.0.0 - External port — Port 18789 must not be reachable from outside
- Firewall — UFW must be active
How I Verified This: Scanning 200 Instances with Shodan
I didn't just trust the defaults — I verified them. Clawdbot uses mDNS for local service discovery, broadcasting as _clawdbot-gw._tcp.local. Shodan indexes these mDNS responses, which allowed me to find real-world instances.
I wrote a scraper that found 200 Clawdbot instances (85% hosted on Hetzner), then probed each one to test whether the WebSocket gateway was accessible and whether authentication was enforced:
async def probe_websocket(ip: str, port: int) -> Dict[str, Any]:
"""Test if WebSocket requires authentication"""
for url in [f"wss://{ip}:{port}/gw", f"ws://{ip}:{port}/gw"]:
try:
async with websockets.connect(url, ssl=ssl_context) as ws:
msg = await asyncio.wait_for(ws.recv(), timeout=5)
if "connect.challenge" in msg:
return {"status": "auth_required"}
return {"status": "connected", "response": msg[:200]}
except:
continue
return {"status": "unreachable"}
The results confirm the defaults work:
| Status | Count | Percentage |
|---|---|---|
| Unreachable (firewalled) | 192 | 96% |
| Auth Required (challenge-response) | 8 | 4% |
| No Auth | 0 | 0% |
Zero unauthenticated instances. If you follow the setup in this guide, your instance will match the 96% that are completely firewalled. The full research is at Validating Clawdbot Security with Shodan.
Step 9: Prompt Injection — Know the Risk
VPS hardening protects against network-level attacks. But Clawdbot has a second attack surface that's fundamentally different: prompt injection.
Clawdbot is an AI agent with real capabilities — shell access, file I/O, messaging, web requests, and persistent memory. A successful prompt injection doesn't just produce bad output. It can execute commands, steal data, send messages as you, or establish persistence via cron jobs and memory files.
How It Works
Prompt injection attacks don't require SSH access. They arrive through the data your agent processes:
- Messages in group chats — A malicious group member crafts a message that tricks the bot into running commands
- Web pages — Hidden instructions on a page the bot fetches
- Documents — Malicious instructions embedded in files the bot reads
- API responses — Poisoned data from external services
The agent processes this untrusted content and may follow the embedded instructions without realizing they're malicious.
What the Research Shows
I reviewed 150+ academic papers on prompt injection defenses. The findings are sobering — read the full analysis here. Key numbers:
- Single defenses achieve only 45-60% effectiveness
- Multi-layered defenses reach 87-94% — better, but still not complete
- Adaptive attackers bypass single-layer defenses 64-68% of the time
- Lab-to-production degradation is 15-30% — real-world performance is worse than benchmarks
No current defense is comprehensive enough to fully prevent prompt injection against an AI agent with tool access.
Practical Mitigations
While there's no silver bullet, you can reduce risk:
- Lock down DM access — Use
dmPolicy: "pairing"so only approved users can trigger the bot. This is the single most important setting. - Use group allowlists — Don't let the bot process messages from unknown groups.
- Require mentions in groups —
requireMention: trueprevents the bot from processing every message in a group. - Run
clawdbot security audit— The built-in audit catches common misconfigurations. - Monitor the agent's activity — Check logs for unexpected commands, outbound connections, or message sends to unknown recipients.
- Be cautious with untrusted data — If you ask the bot to summarize a webpage or read a document, be aware that the content could contain injection attempts.
The full prompt injection research is available at Prompt Injection Defenses: What the Research Actually Shows.
Quick Reference Checklist
Before Clawdbot:
[ ] Update system packages
[ ] Create non-root user with sudo
[ ] Set up SSH key authentication
[ ] Harden sshd_config (disable root + passwords)
[ ] Test key login in a separate terminal
[ ] Enable UFW (SSH only, rate limited)
[ ] Install and configure fail2ban
[ ] Enable unattended-upgrades
Install Clawdbot:
[ ] Install Node.js 22
[ ] Install Clawdbot and run onboarding
[ ] Enable systemd service with linger
[ ] Approve DM pairing for your account
[ ] Verify: clawdbot status / health / doctor
Validate:
[ ] Run: clawdbot security audit --fix
[ ] Run self-audit script (gist)
[ ] Confirm gateway binds to 127.0.0.1
[ ] Confirm UFW is active, port 18789 closed
[ ] Review channel allowlists
Conclusion
A Clawdbot VPS only exposes one port: SSH. By hardening SSH (key-only auth, no root), adding a firewall (UFW), and installing fail2ban with progressive banning, you eliminate the network-level attack surface.
Clawdbot's own defaults handle the gateway — it stays on localhost, invisible to the internet. The built-in security audit and the self-audit script give you one-command verification. And the Shodan research confirms it: across 200 real-world instances, zero were found running without authentication.
The remaining risk — prompt injection — is an industry-wide challenge with no complete solution yet. Use access controls, allowlists, and monitoring to minimize your exposure.
Related Posts
- Validating Clawdbot Security with Shodan — Scanning 200 instances to verify they're secured
- Prompt Injection Defenses: What the Research Actually Shows — Evidence-based analysis of 150+ papers