LAMPSEC
← Back to home

From Wide Open to Zero Trust: Hardening an OpenClaw Server in One Afternoon

*This is a public, redacted write-up of an actual hardening session. All identifiers, IPs, hostnames, keys, tokens, and user details have been replaced with placeholders like 203.0.113.0/24, example.com, and [REDACTED]. Treat this as a practitioner’s field notes—not a compliance checklist.*

Why this exists

When you spin up a fresh server for automation—especially something like **OpenClaw**, which is designed to orchestrate tools, run jobs, and touch lots of systems—you’re creating a high-leverage target.

A default VPS is usually “fine” for running a blog. It’s *not* fine for running an autonomous agent that:

The goal for the afternoon was simple:

  1. Reduce exposed surface area.
  2. Make brute force and opportunistic scanning mostly pointless.
  3. Put **everything** behind a **Zero Trust access path** (WireGuard).
  4. Add continuous visibility: “Did anything change? Did something get vulnerable? Is anything failing?”

This guide walks through the journey in the order we did it, including the parts that were annoying.

---

1) Starting Point: the fresh OpenClaw server reality check

A fresh server build looks deceptively calm. You SSH in, everything works, nothing is on fire.

But security-wise, “fresh” usually means:

From an attacker’s perspective, this is easy mode: scan, banner-grab, try common creds, look for known CVEs, move on.

From an operator’s perspective, it’s also easy mode—until you realize “easy to deploy” is often “easy to compromise.”

The first thing I did was treat the server like it was already hostile territory: assume it will be scanned constantly, and plan accordingly.

---

2) OS-Level Hardening: lock the front door before reorganizing the house

2.1 SSH: key-only, minimal attack surface

The fastest win on most internet-exposed servers is to make SSH boring:

Example (/etc/ssh/sshd_config or a drop-in under /etc/ssh/sshd_config.d/):


PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no

# Optional: only allow a service/admin account
AllowUsers clawadmin

Then:


sudo sshd -t
sudo systemctl reload ssh

**What went wrong (and what I learned):**

2.2 fail2ban: make brute force a waste of time

SSH will get hammered. Not “maybe.” It will.

fail2ban is not a silver bullet, but it turns commodity noise into self-cleaning logs.

We tuned it in two layers:

Example structure (conceptual—adapt to your distro):

/etc/fail2ban/jail.d/sshd.local


[sshd]
enabled = true
port = ssh
backend = systemd

# Baseline: 3 hits in 24h
findtime = 24h
maxretry = 3
bantime  = 24h

# Ban action should match your firewall stack
banaction = ufw

Then add a second jail for “slow brute force” (implementation varies; one approach is a separate filter/jail targeting auth failures with longer findtime). Conceptually:


[sshd-long]
enabled = true
port = ssh
backend = systemd
findtime = 7d
maxretry = 2
bantime = 30d
banaction = ufw

Validate with:


sudo fail2ban-client status
sudo fail2ban-client status sshd

**Opinionated note:** If you have WireGuard and plan to close SSH publicly, fail2ban becomes less critical. But it’s still useful during transition, and it protects you if you temporarily re-open SSH.

2.3 UFW: explicit allowlist, not wishful thinking

I prefer “default deny inbound” because it forces you to enumerate what’s actually allowed.

Example (before WireGuard consolidation):


sudo ufw default deny incoming
sudo ufw default allow outgoing

# Temporary: allow SSH while we’re still using it publicly
sudo ufw allow 22/tcp comment 'SSH (temporary)'

# If you host HTTPS directly
sudo ufw allow 443/tcp comment 'HTTPS'

# WireGuard (eventually the only public ingress besides HTTPS)
sudo ufw allow 51820/udp comment 'WireGuard'

sudo ufw enable
sudo ufw status verbose

**Rule of thumb:** if you can’t explain *why* a port is open, it shouldn’t be.

2.4 Kernel hardening: small sysctls, real signal

Two settings I like early:

Example /etc/sysctl.d/99-hardening.conf:


# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Don’t allow setuid programs to produce core dumps
fs.suid_dumpable = 0

Apply:


sudo sysctl --system

This isn’t “maximum security.” It’s “remove footguns and increase visibility.”

2.5 Passwordless sudo for a service account (yes, but do it deliberately)

This one is controversial.

For an autonomous agent server, there’s a real operational tension:

What we did was create (or use) a dedicated service/admin account (e.g., openclaw) and allow passwordless sudo for a *minimal* set of commands (ideally), or full sudo only if you accept the risk.

Preferred: lock it down to specific commands:

/etc/sudoers.d/openclaw


# Example: allow only service management for specific units
openclaw ALL=(root) NOPASSWD: /bin/systemctl restart openclaw*, /bin/systemctl status openclaw*

If you must do broader permissions, treat it as a temporary phase while you build a better privilege model (polkit rules, separate automation runner, etc.).

---

3) Cloud Provider Firewall: the rules above your rules

Here’s a mistake people make (including me, historically):

“I set UFW, so I’m protected.”

Cloud firewalls exist *above* your server firewall. They can block traffic before it ever hits your NIC. That’s good.

We used the **cloud hosting API** to manage firewall rules programmatically. The goal wasn’t just “click in a UI once.” It was:

3.1 Set up API access for automation

Create API credentials in your cloud provider dashboard and store them securely (not in shell history, not in a public repo).

Use placeholders in scripts:

Store in a root-readable secret file or a dedicated secrets manager:

3.2 Remove unnecessary ports (goodbye, port 80)

If you’re not serving HTTP, close it.

If you *are* serving HTTP, consider redirecting 80→443 at a reverse proxy *only if needed*. In many hardened setups, you can skip 80 entirely and use:

In our case, we removed 80/tcp at the provider firewall.

**Why provider-level removal matters:**

3.3 Programmatic firewall management (pattern)

I’m not including exact endpoints/IDs from our environment (public write-up rule), but the pattern is:

  1. List resources (datacenters, servers, NICs).
  2. Identify the NIC attached to your public IP.
  3. Fetch firewall rules for that NIC.
  4. Apply a desired allowlist:

A minimal pseudo-script outline:


export HOSTING_PROVIDER_USER='[YOUR-HOSTING_PROVIDER-API-USER]'
export HOSTING_PROVIDER_PASS='[YOUR-HOSTING_PROVIDER-API-PASSWORD]'

# 1) discover resources
# 2) compute desired firewall allowlist
# 3) PUT/PATCH firewall rules
# 4) verify effective rules

**Lesson learned:** API rate limits and eventual consistency are real. Build scripts that:

---

4) Docker Service Isolation: if it must be messy, keep it contained

We moved “risky-but-useful” services into Docker:

Docker doesn’t magically secure things, but it gives you primitives:

4.1 The big rule: torrent traffic must never escape the VPN

If you run torrents, your threat model changes. Even if you’re legally fine, you’re now:

So we forced torrent traffic through a **WireGuard/VPN tunnel interface**.

The principle:

4.2 Practical pattern: VPN container + dependent services

A common approach:

High-level docker-compose.yml sketch:


services:
  vpn:
    image: example/wireguard-client:latest
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    volumes:
      - ./wireguard:/config:ro

  downloader:
    image: example/download-client:latest
    network_mode: "service:vpn"
    depends_on:
      - vpn
    volumes:
      - ./data:/data

**Important:** Don’t publish downloader web UIs to the public interface. Either:

4.3 Containers aren’t a security boundary (but they help)

My posture:

---

5) Automated Vulnerability Scanning: make drift visible

You can harden perfectly at 3pm and be vulnerable again at 3am because:

So we set up continuous scanning in three layers:

  1. **Docker image update checker**: “Are there newer images than what I’m running?”
  2. **GitHub release comparison**: “Did upstream ship a new release?”
  3. **CVE lookups via OSV**: “Do my dependencies map to known vulnerabilities?”

5.1 OSV API: fast, scriptable CVE intelligence

OSV (Open Source Vulnerabilities) provides a clean API for querying vulnerabilities by package/ecosystem.

Example (conceptual curl):


curl -s https://api.osv.dev/v1/query \
  -H 'Content-Type: application/json' \
  -d '{
    "package": {"name": "openssl", "ecosystem": "Ubuntu"},
    "version": "[INSTALLED-VERSION]"
  }'

5.2 Run it every 12 hours (cron)

We scheduled a 12-hour cron job for the scanner. The job should:

Skeleton cron entry:


# Twice daily vulnerability scan
0 */12 * * * /usr/local/bin/security-scan.sh >> /var/log/security-scan.log 2>&1

**Opinionated note:** Don’t make the scanner “auto-patch” everything. Auto-patching is how you turn a security program into an uptime incident generator. Alert first, patch intentionally.

---

6) Infrastructure Monitoring: treat “boring” as a feature

Security without monitoring is performance art.

We added cloud provider monitoring policies for:

Thresholds depend on your workload, but a pragmatic baseline is:

**Lesson learned:** monitoring isn’t about catching hackers like in movies. It’s about catching:

Attackers benefit from your silence. Monitoring is you refusing to be silent.

---

7) WireGuard Zero-Trust Access: move the perimeter to *identity*

This was the centerpiece: stop exposing internal services to the public internet.

The model:

7.1 Set up the WireGuard server

Install WireGuard on the server and generate keys.

Example (commands vary by distro):


sudo apt-get update
sudo apt-get install -y wireguard

wg genkey | tee /etc/wireguard/server.key | wg pubkey | tee /etc/wireguard/server.pub

Create /etc/wireguard/wg0.conf (redacted example):


[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = [REDACTED]

# Enable forwarding / NAT as needed
PostUp = [REDACTED IPTABLES/NFT RULES]
PostDown = [REDACTED IPTABLES/NFT RULES]

[Peer]
PublicKey = [CLIENT-1-PUBKEY]
AllowedIPs = 10.8.0.2/32

Enable:


sudo systemctl enable --now wg-quick@wg0
sudo wg show

7.2 Generate client configs (and don’t leak them)

Client config example:


[Interface]
PrivateKey = [REDACTED]
Address = 10.8.0.2/32
DNS = 10.8.0.1

[Peer]
PublicKey = [SERVER-PUBKEY]
Endpoint = example.com:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25

7.3 Split tunnel vs full tunnel

We used **split tunneling** for admin access so that:

This is a practical compromise for day-to-day operations.

If you want maximum privacy, you can do full tunnel (AllowedIPs = 0.0.0.0/0, ::/0), but then you inherit a different set of problems (DNS, exit routing, bandwidth, troubleshooting complexity).

7.4 Move ALL services behind VPN

This is the part that changes your security posture overnight.

Actions:

And finally:

That last parenthetical is not optional.

**What went wrong (and why it matters):**

  1. bring up WireGuard,
  2. verify connectivity,
  3. verify you can recover if it fails,
  4. then close SSH and app ports.

Zero Trust is not a vibe. It’s a sequence of reversible steps.

---

8) The Result: from 5+ open ports to 2

The “before” picture (typical):

The “after” picture:

Everything else:

This is the real win: your attack surface isn’t “hardened,” it’s **gone**.

What’s left to do (the honest backlog)

Hardening is never “done.” The shortlist I’d do next:

---

9) Security Scanning Pipeline: automate the paranoia

Once you have a baseline, automation keeps it from drifting.

We wired up a pipeline conceptually like this:

9.1 Llama Guard 4 for content classification

If your agent ingests external text (issues, PRs, forum posts, chat logs), you want a filter before that text becomes “instructions.”

We used a content classifier step (Llama Guard 4) to label inbound content and flag:

Key idea: **don’t let untrusted text become execution context without a gate.**

9.2 Automated security scans every 6 hours

In addition to the 12-hour vuln scan, we scheduled lighter “health/security drift” checks every 6 hours:

This catches the “I installed something and forgot” class of incidents.

9.3 HIBP credential checks

If your workflows involve credentials, periodically check whether they’ve appeared in known breaches.

Important notes for doing this responsibly:

Public write-up placeholder:

9.4 LinPEAS weekly audits

LinPEAS is noisy, but it’s good at finding:

Run it weekly and treat it like a diff tool: what changed since last week?

---

10) Lessons Learned: the stuff I’d tell past-me

10.1 Commit immediately

Every time you make a security change, you’re editing reality.

If your code/config isn’t committed:

Security work without version control is just vibes and luck.

10.2 Test the VPN path before closing ports

Make this a ritual:

  1. Bring up WireGuard.
  2. Connect a client.
  3. SSH over the VPN.
  4. Confirm you can reach service UIs over the VPN.
  5. Only then close public ports.

If you do it in the wrong order, you don’t get “better security.” You get “an unplanned outage with a side of self-inflicted lockout.”

10.3 Cloud firewalls exist above OS firewalls

Provider firewall rules are your first line.

UFW/nftables on the box is your second.

Docker’s port publishing is a third.

You want all three aligned.

10.4 Rate limits and eventual consistency are part of infrastructure

If you automate firewall changes via API:

10.5 Security is a system, not a checklist

The biggest shift wasn’t a sysctl tweak. It was changing the shape of the system:

That’s the difference between “a hardened box” and “an infrastructure that stays hardened.”

---

Appendix: Quick “After” checklist (redacted)

Use this as a sanity scan after you implement the above.

---

Closing

Hardening an OpenClaw server isn’t about winning a theoretical security contest. It’s about reducing the number of ways your future self can have a very bad day.

In one afternoon, we went from “internet-exposed services and hope” to “two ports, VPN access, scanning, monitoring, and a clear upgrade path.”

If you take only one idea from this post:

Don’t harden by adding more rules. Harden by deleting exposure.