Here’s the truth: a freshly provisioned VPS is a target in a shooting gallery. Bots are scanning your IP right now, probing for default credentials and known vulnerabilities. If you’re deploying OpenClaw to production, you need to move fast and harden hard—not six months from now, but today.
We’re going to walk through a layered defense strategy that eliminates the low-hanging fruit, then adds detection and response layers that actually work. SSH key-only auth, UFW firewall rules, fail2ban automation, CrowdSec intelligence, and unattended security updates. By the end of this, your VPS will look actively hostile to attackers—just the way we like it.
Why This Matters (The Threat Model)
Let me be direct: password-based SSH is a dead protocol. It gets brute-forced constantly. Default ports get hammered thousands of times per hour. Default credentials are in every attacker’s playbook. Public-facing services without firewalls are honeypots with neon signs.
For OpenClaw, we need:
- SSH access that cannot be compromised by guessing or dictionary attacks
- Firewall rules that block everything except what you explicitly allow
- Automatic detection and rate-limiting of suspicious connection attempts
- Real-time threat intelligence that stops known malicious IPs
- Automated patching so zero-days don’t linger on your system
This isn’t paranoia. This is baseline hygiene. Let’s do it.
Step 1: SSH Hardening – Keys Only, Custom Port
SSH is the door to your VPS. The default configuration—password authentication on port 22—is like leaving that door unlocked. Every bot on the internet knows exactly where to find it and how to probe it. Within minutes of your VPS coming online, thousands of automated attacks will be hammering against SSH, trying default usernames and password combinations. We need to change this fundamentally.
Why SSH Keys Matter (The Why)
Here’s what happens with password-based SSH: attackers don’t need to guess your password perfectly. They just need to try enough combinations. A botnet with a million machines running in parallel can try billions of password combinations per second. Your password might be 12 characters, but against computational firepower like that, it’s not security—it’s theater.
SSH keys, by contrast, are cryptographic. An Ed25519 key is a 256-bit elliptic curve key. There’s no password to guess. There’s no brute force that works. An attacker can try a million keys; none of them will authenticate. The only way in is with your actual private key, which lives on your local machine, safely disconnected from the internet.
Additionally, passwords are transmitted, logged, and potentially intercepted. Keys are never transmitted—only the cryptographic proof that you possess the key.
Generate Your SSH Keypair (Local Machine)
Start on your local machine. You’re going to create an SSH keypair, and you’ll keep the private key as jealously guarded as your password. This is a one-time operation, but it’s foundational.
ssh-keygen -t ed25519 -C "openclaw-prod" -f ~/.ssh/openclaw_prod
This creates two files:
~/.ssh/openclaw_prod– your private key (keep this secret, like a password)~/.ssh/openclaw_prod.pub– your public key (goes on the VPS)
Don’t use RSA unless you have a very old system. Ed25519 is faster, smaller, more resistant to side-channel attacks, and genuinely more secure. If prompted for a passphrase, use one. Your private key encrypted at rest—protected by a passphrase you type when you use it—is dramatically better than an unencrypted key sitting on disk.
When you enter a passphrase, ssh-keygen will encrypt your private key with that passphrase. Then, when you SSH, you’ll enter the passphrase once, and your SSH agent will cache the decrypted key for the duration of your session. This is the sweet spot: security without constant re-authentication friction.
Add Your Public Key to the VPS
You’ll SSH in once with password auth to set up key-based auth. After that, password auth is gone forever. Log into your VPS via the provider’s console (Vultr, Linode, DigitalOcean, AWS EC2, wherever you’re hosting).
If you’re using a cloud provider’s web console, you’ll be able to paste commands directly:
# On the VPS, as root or your initial user
mkdir -p ~/.ssh
echo "ssh-ed25519 AAAA... (your public key content)" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh
Here’s what each command does:
mkdir -p ~/.ssh– Creates the.sshdirectory if it doesn’t exist. The-pflag means “don’t error if it already exists.”echo "..." >> ~/.ssh/authorized_keys– Appends your public key to theauthorized_keysfile. Note the>>(append), not>(overwrite). If you use>, you’ll delete any existing keys.chmod 600 ~/.ssh/authorized_keys– Sets permissions so only the owner can read/write the file. SSH will refuse the file if it’s world-readable.chmod 700 ~/.ssh– Sets permissions on the directory itself so only the owner can access it. SSH is strict about this.
Paste your public key—the literal contents of ~/.ssh/openclaw_prod.pub on your local machine—into that authorized_keys file. The whole line, starting with ssh-ed25519.
If you have multiple machines you’ll SSH from, add multiple keys to authorized_keys, one per line. This is how you grant access from multiple computers.
Configure sshd for Key-Only Auth
Now we’re going to lock down the SSH daemon itself. Edit /etc/ssh/sshd_config:
sudo nano /etc/ssh/sshd_config
This file controls SSH’s behavior. Find and modify (or add) these lines:
# Change the port from 22 to something less obvious
Port 2742
# Disable password-based authentication entirely
PasswordAuthentication no
PubkeyAuthentication yes
# Disable empty passwords (shouldn't exist anyway)
PermitEmptyPasswords no
# Disable root login via SSH
PermitRootLogin no
# Disable X11 forwarding (reduces attack surface)
X11Forwarding no
# Keep connections alive (detect dead clients)
ClientAliveInterval 300
ClientAliveCountInterval 3
# Don't allow users to set environment variables (prevents injection)
PermitUserEnvironment no
# Limit authentication attempts
MaxAuthTries 3
MaxSessions 10
Let me explain each setting and why it matters:
Port 2742: It’s not magic—any port above 1024 works. The idea is simple: automated scanners look for SSH on port 22 by default. They probe millions of IPs, looking for port 22 specifically. If your SSH is on 2742, they’ll miss you. Combined with key-only auth, you’ve eliminated 99% of the noise. Yes, this is security through obscurity, but obscurity as a layer on top of cryptographic key auth is fine. It’s the combination that counts.
PasswordAuthentication no: This is non-negotiable. You’ve just set up key auth; now disable password auth entirely. No fallback. No “well, maybe I can guess this password.” No password. Only keys. This blocks dictionary attacks, credential stuffing, and brute force.
PubkeyAuthentication yes: Explicitly enable key authentication. Modern SSH defaults to “yes,” but be explicit.
PermitRootLogin no: Root login over SSH is a gift to attackers. Even with key-only auth, if someone gains your key, they shouldn’t get root. Force login as a regular user, then sudo if needed. This is defense in depth.
X11Forwarding no: X11 forwarding allows graphical applications to run remotely. It’s rarely needed in production and adds complexity. Disable it.
ClientAliveInterval and ClientAliveCountInterval: These settings prevent dead connections from hanging forever. If your client crashes, the server will notice and close the connection within a few minutes. This prevents SSH zombies.
PermitUserEnvironment no: Users can set environment variables through SSH. This can be exploited for all sorts of mischief. Disable it.
MaxAuthTries 3: After 3 failed authentication attempts, the connection closes. This prevents accidental lockouts but stops brute force attempts even faster than fail2ban can.
MaxSessions 10: Limit concurrent sessions per user. This prevents one compromised key from opening a thousand shells.
Restart SSH and Test (Critical)
We’ve changed SSH’s configuration. Now restart it:
sudo systemctl restart ssh
CRITICAL NEXT STEP: Do not close your current SSH session. If something goes wrong and you’ve locked yourself out, you’ll need the provider’s web console to fix it. This is not a theoretical risk—it happens constantly.
Open a new terminal and test logging in with your key from your local machine:
ssh -i ~/.ssh/openclaw_prod -p 2742 your-user@your-vps-ip
If this works, you’ve succeeded. If not, you’ll get an error. Common errors:
- “Connection refused”: SSH service isn’t running, or the port is wrong. Go back to
sshd_configand verify. - “Permission denied (publickey)”: Your key isn’t in
authorized_keys, or permissions are wrong. Check the.sshdirectory permissions. - “Could not resolve hostname”: Your IP address is wrong. Verify your VPS IP with your provider.
If you get an error, don’t disconnect your original session. Use the provider’s web console to fix the issue, or use your original working session to debug. Once you’ve confirmed that key access works from a new session, then you can disconnect the old one.
Why this paranoia? Because one wrong move leaves you unable to access your VPS except through the provider’s web console. The web console is slow and often needs a “hard reset” to recover. A few minutes of testing now saves hours later.
Update Your SSH Config (Local)
Create an entry in ~/.ssh/config so you don’t have to remember the port and key path:
Host openclaw-prod
HostName your-vps-ip
Port 2742
User your-username
IdentityFile ~/.ssh/openclaw_prod
StrictHostKeyChecking accept-new
Now you can just ssh openclaw-prod and it uses all the right settings.
Step 2: UFW Firewall – Block Everything, Allow Explicitly
Your VPS is online. SSH is hardened. Now we need a firewall that blocks everything by default and only opens what you explicitly need.
UFW (Uncomplicated Firewall) is a frontend for iptables—a lower-level Linux firewall tool that’s powerful but confusing. UFW makes it simple: deny by default, allow specific traffic. Think of it as a bouncer who says “no” to everyone unless you’ve put them on the list.
Initialize UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
This is your firewall’s foundational policy:
- deny incoming: All inbound connections are blocked by default. Bots, scanners, random traffic—all blocked.
- allow outgoing: Your VPS can reach out to the internet. This is essential—you need to download packages, pull updates, reach your monitoring systems.
- enable: Actually turn the firewall on. Without this, the rules above are configured but not enforced.
Your VPS can now reach out to the world, but the world can’t reach in unless you explicitly allow it.
Allow SSH (on Your Custom Port)
sudo ufw allow 2742/tcp
This opens your SSH port. Note tcp to be explicit—we’re not allowing UDP here. UDP is connectionless and doesn’t make sense for SSH. Also, note the exact port: 2742. If you changed it to something else in sshd_config, use your port here.
If you don’t do this, you’ll lock yourself out of your own VPS. This is why testing is critical (see below).
Allow Tailscale (We’ll Use This Later)
If you’re planning to use Tailscale for a zero-public-ports setup (covered in Article 2), allow its traffic now:
sudo ufw allow in on tailscale0
This allows all traffic on the Tailscale interface. We’ll bind OpenClaw to Tailscale’s IP only, so this is safe.
Allow Required Application Ports (Carefully)
For OpenClaw, you might need HTTP/HTTPS if you’re exposing it publicly. But here’s the thing: don’t. Use Tailscale instead. If you must expose ports, be specific:
# Only if you're NOT using Tailscale
# sudo ufw allow 80/tcp
# sudo ufw allow 443/tcp
Don’t do this unless necessary. The beauty of Tailscale is you don’t need public ports at all.
Verify Your Rules
sudo ufw status verbose
Output should look like:
Status: active
To Action From
-- ------ ----
2742/tcp ALLOW Anywhere
Anywhere on tailscale0 ALLOW Anywhere
Anywhere (v6) on tailscale0 ALLOW Anywhere (v6)
Good. Everything else is blocked.
Update Rules If Needed
# Delete a rule
sudo ufw delete allow 8080/tcp
# Insert a rule at position
sudo ufw insert 1 allow from 192.168.1.0/24 to any port 22
Keep your firewall rules minimal. Every open port is a potential vulnerability.
Step 3: fail2ban – Automated Attack Response
Your firewall blocks most traffic. Your SSH is hardened to key-only auth. But what about the attacks that slip through? What about the legitimate-looking probes? What about the brute force attempts against your SSH port, even though passwords don’t work?
fail2ban is your automated response layer. It watches your log files in real-time. When it detects suspicious patterns—for example, five failed SSH login attempts in ten minutes—it automatically adds a firewall rule to block that IP. It’s like having a smart bouncer at your door who remembers troublemakers and bans them automatically.
Here’s the power of fail2ban: even though password auth is disabled, attackers will still try. Botnets will hammer your SSH port with credential combinations. fail2ban watches them fail, counts the failures, and bans the attacking IP. The attacker wastes energy; you block them automatically.
Install fail2ban
sudo apt update
sudo apt install fail2ban
Create a Local Configuration
Don’t edit /etc/fail2ban/jail.conf directly—that’s the default configuration managed by the package. Instead, create /etc/fail2ban/jail.local to override defaults. This way, your customizations survive package updates:
sudo nano /etc/fail2ban/jail.local
Add:
[DEFAULT]
# Ban duration: 3600 seconds = 1 hour
bantime = 3600
# Detection window: 600 seconds = 10 minutes
findtime = 600
# Ban after 5 failed attempts within findtime
maxretry = 5
# Email notifications (optional; requires postfix)
destemail = [email protected]
sendername = fail2ban
mta = postfix
[sshd]
enabled = true
port = 2742
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
[sshd-ddos]
enabled = true
port = 2742
filter = sshd-ddos
logpath = /var/log/auth.log
maxretry = 10
findtime = 10
bantime = 600
Let me explain what’s happening here:
[DEFAULT]: These are defaults applied to all jails unless overridden.
bantime = 3600: How long (in seconds) to block an attacking IP. 3600 = 1 hour. After 1 hour, the ban expires automatically and the IP can try again. This is long enough to stop active attacks but short enough to be reasonable if a legitimate user misconfigures something.
findtime = 600: The detection window. We count failures within this 10-minute window. If you get 5 failed attempts within 10 minutes, we react.
maxretry = 5: Ban after this many failures within findtime. The DEFAULT here is 5, but we override it in the SSH jail.
[sshd] jail: This monitors SSH-specific logs for login failures.
maxretry = 3: Here we’re stricter—only 3 failed attempts before banning. Why? Because with key-only auth, a legitimate user won’t fail SSH logins. Failures mean attackers.
[sshd-ddos] jail: This is separate. It detects rapid connection attempts—10 attempts within 10 seconds. This catches port scanners and botnets that are just probing for SSH, not even trying to authenticate. They get a shorter ban (600 seconds = 10 minutes) because they’re less of a threat than actual brute force.
These two jails work together: one catches patient brute forcers, one catches rapid scanners.
Restart fail2ban
sudo systemctl restart fail2ban
Monitor Bans in Real-Time
sudo fail2ban-client status sshd
Output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 42
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 2
|- Total banned: 15
`- IP list: 203.0.113.45 198.51.100.78
Those banned IPs can’t connect to SSH until the ban time expires (or you lift it manually).
Check Active Bans
sudo iptables -L | grep fail2ban
Or to see all bans:
sudo fail2ban-client set sshd unbanip 203.0.113.45
This unban command is useful if you accidentally block yourself (or a legitimate user).
Step 4: CrowdSec – Real-Time Threat Intelligence
fail2ban is reactive—it watches your logs and responds to attacks on your system. But what if we could be proactive? What if we could block IPs before they even attack you, because they’re known to be malicious somewhere else in the world?
That’s CrowdSec. It’s a crowdsourced threat intelligence platform. Thousands of deployed instances contribute attack data to a central system. Your VPS subscribes to this intelligence. An IP attacking someone in Germany gets added to the blocklist. Your VPS in the US learns about it within minutes. The IP tries to attack you, but you’ve already blocked it.
It’s like a neighborhood watch, but global, and instant.
Install CrowdSec
curl -s https://install.crowdsec.net | sudo sh
This installs CrowdSec and automatically integrates it with your UFW firewall. CrowdSec feeds UFW the blocklist.
On first run, CrowdSec will prompt you to create an account. Do it—this account enrolls your instance in the community and allows you to receive threat intelligence.
Configure CrowdSec
CrowdSec’s main config is at /etc/crowdsec/config.yaml. The defaults are reasonable, but let’s review what’s happening:
sudo nano /etc/crowdsec/config.yaml
The key sections are:
# Acquire logs from journalctl
acquisition:
filenames:
- /var/log/auth.log
- /var/log/syslog
# Parse SSH logs for suspicious activity
parsers:
- crowdsecurity/sshd-logs
# Detect brute force and scanning attacks
scenarios:
- crowdsecurity/ssh-bf
# Block IPs with UFW when attacks detected
remediation:
- crowdsecurity/ufw-ban
Here’s what’s happening:
acquisition: CrowdSec reads your auth logs and syslog. It’s looking at the same data fail2ban is, but with more sophisticated pattern recognition.
parsers: These extract structured data from logs. The sshd-logs parser understands SSH log format and extracts who’s trying to log in, from where, and whether they succeeded.
scenarios: These are attack patterns. ssh-bf detects brute force attacks—multiple failed attempts from the same IP. CrowdSec’s heuristics are more sophisticated than fail2ban’s rules.
remediation: When an attack is detected, CrowdSec tells UFW to block the IP. This is the actual enforcement.
The beauty of this setup is that UFW, fail2ban, and CrowdSec all feed into the same firewall rules. They reinforce each other.
Enroll Your Instance (Community Sharing)
sudo cscli console enroll -u <your-username> -p <your-password>
This connects your VPS to the CrowdSec network. When you enroll, you:
- Receive threat intelligence: IPs known to be malicious globally
- Contribute anonymized attack data: Help others by sharing what attacks you see
You’re not exposing your infrastructure details. CrowdSec sends only hashed, anonymized data. Your privacy is protected, and the community gets better threat intelligence.
Start CrowdSec
sudo systemctl start crowdsec
sudo systemctl enable crowdsec
Monitor Decisions
sudo cscli decisions list
This shows all IPs currently blocked by CrowdSec decisions (both local and community-based).
Example output:
ID │ IP │ TYPE │ ACTION │ REASON │ DURATION │ ORIGIN
────┼─────────────────┼──────────┼─────────┼───────────────────┼───────────┼─────────────
1 │ 192.0.2.55 │ IP │ ban │ ssh-bf │ 4h │ local
2 │ 198.51.100.12 │ IP │ ban │ ssh-bf │ 4h │ local
3 │ 203.0.113.99 │ IP │ ban │ crowdsecurity │ 24h │ community
See that last one? An IP from somewhere in the world is attacking someone, CrowdSec knows about it, and your firewall blocks it automatically. That’s defense at scale.
Step 5: Lynis – Security Audit and Hardening
You’ve hardened SSH, set up a firewall, enabled fail2ban, and added threat intelligence. But are there other vulnerabilities lurking? Unpatched packages? Misconfigured permissions? Unneeded services?
Lynis is a security auditing tool that does exactly what its name suggests: it comprehensively audits your system for security issues. It’s like a professional security consultant that runs automatically.
Lynis scans your system for:
- Unpatched software
- Weak file permissions
- Disabled security features
- Unneeded services running
- Insecure configurations
- Missing security tools
It produces a report with recommendations, prioritized by severity.
Install Lynis
sudo apt install lynis
Run a Full Audit
sudo lynis audit system
This takes a few minutes and produces a detailed report. You’ll see output like:
[+] System Binaries and Permissions
[!] File permissions: /bin/nc - 755
[!] File permissions: /bin/netcat - 755
[+] Available Security Tools
[+] Wireshark: found
[+] Chkrootkit: not found
[+] Logging and Auditing
[!] auditd not enabled
[!] Warning: Some important system tools are missing
The [+] prefix means “good” (something passed). The [!] prefix means “warning” (something needs attention). The suggestions range from “you should review this” to “this is actually a security risk.”
Focus on the critical ones. Some recommendations (like installing Chkrootkit) are optional—it’s a rootkit detector that’s rarely needed on a properly configured system. But if Lynis says a critical permission is wrong, fix it.
Create a Cron Job for Regular Audits
sudo crontab -e
Add:
0 2 * * 0 /usr/sbin/lynis audit system --quiet > /var/log/lynis-audit.log 2>&1
This runs Lynis every Sunday at 2 AM and logs output. You can review the log periodically.
Step 6: Unattended Security Updates – Stay Current Automatically
Here’s an uncomfortable truth: every day, vulnerabilities are discovered in Linux packages, in SSH, in your kernel. Zero-day exploits exist. Security patches arrive constantly. If you wait for a human to manually apply them, you’ll lag by weeks or months. In that window, your VPS is vulnerable.
Unattended-upgrades is simple: it automatically patches your system with security updates. It doesn’t touch major version bumps—those require human judgment. But security patches? Those are applied automatically, immediately, as soon as the package manager sees them.
This is not optional. This is baseline hygiene.
Install Unattended-Upgrades
sudo apt install unattended-upgrades apt-listchanges
apt-listchanges shows you what changed in each upgrade (optional but useful for understanding what was patched).
Configure Automatic Updates
Edit /etc/apt/apt.conf.d/50unattended-upgrades:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Ensure these settings are present:
// Allow automatic updates from security repositories only
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
// Automatically reboot if required (optional, but recommended for security)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
// Email notifications of updates applied
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";
// Automatic scheduling and cleanup
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
Here’s what each setting does:
Allowed-Origins: These specify which package sources can auto-update. We’re limiting to -security repositories only. This means security patches are applied automatically, but major version upgrades (which might break things) require manual review.
Automatic-Reboot: Some patches require a reboot (kernel updates, for example). If set to "true", unattended-upgrades will reboot automatically. If this makes you uncomfortable, set it to "false" and handle reboots manually.
Automatic-Reboot-Time: If auto-reboot is enabled, reboot at 3 AM. This is outside typical business hours, minimizing disruption. Adjust to your timezone and usage patterns.
Mail notifications: You’ll get an email when updates are applied. This lets you know your system is being patched and lets you plan for any restarts.
APT::Periodic settings: These enable automatic checking for updates, downloading them, and applying security patches on a regular schedule.
Verify It’s Running
sudo systemctl status unattended-upgrades
Check the log:
sudo tail -f /var/log/unattended-upgrades/unattended-upgrades.log
Step 7: System Hardening – Additional Layers
Beyond SSH and firewall, a few more quick wins:
Disable SSH Agent Forwarding
Edit /etc/ssh/sshd_config again:
AllowAgentForwarding no
AllowTcpForwarding no
These prevent attackers from using your SSH session to pivot deeper into your network.
Enable SELinux or AppArmor
AppArmor is simpler; it’s usually on Ubuntu by default. Check:
sudo aa-status
If it’s running, you’re protected. Profiles should show various services (SSH, etc.). Don’t worry about tuning it unless you hit issues.
Set Resource Limits
Edit /etc/security/limits.conf to prevent DoS via resource exhaustion:
* soft nofile 4096
* hard nofile 65536
* soft nproc 4096
* hard nproc 65536
Enable TCP Wrappers (Optional)
Create /etc/hosts.allow:
sshd: ALL
Create /etc/hosts.deny:
ALL: ALL
This is a legacy layer—UFW already blocks most traffic, but it’s a safe redundancy.
Putting It All Together – Testing Your Hardening
Let’s verify your hardening is working:
Test SSH Lockout
From a different machine, intentionally fail SSH login 5 times:
for i in {1..5}; do ssh -i /wrong/key -p 2742 user@vps 2>&1; done
Then try again:
ssh -i ~/.ssh/openclaw_prod -p 2742 user@vps
You should see a Connection refused or Permission denied because fail2ban blocked the IP. Give it a moment—fail2ban needs time to process logs.
Verify UFW Rules
sudo ufw status numbered
Confirm only your whitelist is there. No surprises.
Check fail2ban Status
sudo fail2ban-client status
sudo fail2ban-client status sshd
See active bans. Bans should persist, auto-expiring after bantime.
Check CrowdSec Alerts
sudo cscli alerts list
sudo cscli decisions list
Should show attacks detected and IPs blocked.
Run Lynis
sudo lynis audit system --quick
Should show fewer warnings than before.
Monitoring and Maintenance
Your hardened VPS needs occasional attention:
Monthly Tasks
- Review fail2ban logs:
sudo tail -50 /var/log/fail2ban.log - Check CrowdSec alerts:
sudo cscli alerts list - Verify UFW rules:
sudo ufw status - Check unattended-upgrades log:
sudo tail -20 /var/log/unattended-upgrades/unattended-upgrades.log
Quarterly Tasks
- Run Lynis:
sudo lynis audit system - Review SSH logs for anomalies:
sudo tail -100 /var/log/auth.log | grep "Failed\|Accepted" - Check system load and resource usage:
top,df -h
Annually
- Update SSH keys if compromised
- Review and tighten UFW rules based on actual usage
- Update all hardening configs as best practices evolve
Common Troubleshooting
“I can’t SSH in after hardening.”
- Forgot your private key path? Check
~/.ssh/config. - Firewall blocking? Check
sudo ufw status. - SSH service crashed? Restart it:
sudo systemctl restart ssh. - Need console access? Use your VPS provider’s web console to fix
sshd_config.
“fail2ban is banning me!”
- Check bans:
sudo fail2ban-client status sshd. - Unban yourself:
sudo fail2ban-client set sshd unbanip YOUR_IP. - Adjust
maxretryif you make mistakes often.
“CrowdSec isn’t blocking anything.”
- Is it running?
sudo systemctl status crowdsec. - Are decisions being made?
sudo cscli decisions list. - Check logs:
sudo tail -50 /var/log/crowdsec/crowdsec.log.
“Unattended-upgrades keeps rebooting at inconvenient times.”
- Change
Automatic-Reboot-Timein/etc/apt/apt.conf.d/50unattended-upgrades. - Or disable auto-reboot and handle it manually:
Unattended-Upgrade::Automatic-Reboot "false".
Wrapping Up
You’ve just built a layered defense that:
- Eliminates password guessing via SSH key-only auth
- Hides your service on a non-standard port
- Blocks mass scans with UFW default-deny
- Responds to attacks automatically with fail2ban
- Leverages community intelligence with CrowdSec
- Stays current with unattended-upgrades
- Audits itself with Lynis
This isn’t Fort Knox, but it’s solid. Attackers move on to easier targets. Your OpenClaw VPS is now genuinely hard to exploit.
The key to maintaining this is consistency. Check your logs. Watch for patterns. Keep learning. Security isn’t a destination; it’s a practice.
In the next article, we’ll take this a step further: zero public ports via Tailscale, so your OpenClaw gateway is completely invisible to the internet. See you there.
The Hardened VPS in Context
You’ve built something real here. Your VPS is no longer a sitting duck. It’s actively defended. Not invulnerable (nothing is), but hostile to attackers. They scan it, find no easy entry, and move to the next target.
The bots that hammered your SSH port? They’re still hitting you, but they’re banging against a locked door. fail2ban is watching them fail and banning them. CrowdSec is cross-referencing known malicious IPs and blocking them preemptively.
Meanwhile, unattended-upgrades is patching your system in the background. Security holes are closing the moment patches land. You’re not waiting for a convenient Tuesday to run updates. The system patches itself.
This is what production security looks like. It’s not paranoid. It’s disciplined. And it’s absolutely necessary for anything publicly exposed to the internet.
When Security Is Overkill (And When It’s Not)
You might be thinking: “Isn’t all this overkill for a single OpenClaw instance?” Fair question. Here’s how to think about it:
If your OpenClaw instance:
- Runs on a public IP without protection: You’re exposed. Do this.
- Handles sensitive data or important workflows: You’re a target. Do this.
- Needs to survive autonomous attacks: You need layers. Do this.
- Is just for personal experimentation on a private network: Some of this is overkill, but do the SSH hardening at minimum.
The beauty of this approach is it’s cost-zero (in actual dollars—just CPU cycles). A hardened VPS costs the same as a wide-open one. Only your effort differs. That’s not overkill; that’s smart.