| title | Security Architecture |
|---|---|
| description | Firewall configuration, Docker isolation, and security hardening details |
This playbook implements a multi-layer defense strategy to secure OpenClaw installations.
# Default policies
Incoming: DENY
Outgoing: ALLOW
Routed: DENY
# Allowed
SSH (22/tcp): ALLOW
Tailscale (41641/udp): ALLOWAutomatic protection against SSH brute-force attacks:
# Configuration
Max retries: 5 attempts
Ban time: 1 hour (3600 seconds)
Find time: 10 minutes (600 seconds)
# Check status
sudo fail2ban-client status sshd
# Unban an IP
sudo fail2ban-client set sshd unbanip IP_ADDRESSCustom iptables chain that prevents Docker from bypassing UFW:
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -i lo -j ACCEPT
-A DOCKER-USER -i <default_interface> -j DROP
COMMIT
Result: Even docker run -p 80:80 nginx won't expose port 80 externally.
All container ports bind to 127.0.0.1:
ports:
- "127.0.0.1:3000:3000"Container processes run as unprivileged openclaw user.
The openclaw service runs with security restrictions:
NoNewPrivileges=true- Prevents privilege escalationPrivateTmp=true- Isolated /tmp directoryProtectSystem=strict- Read-only system directoriesProtectHome=read-only- Limited home directory accessReadWritePaths- Only ~/.openclaw is writable
The openclaw user has limited sudo permissions (not full root):
# Allowed commands only:
- systemctl start/stop/restart/status openclaw
- systemctl daemon-reload
- tailscale commands
- journalctl for openclaw logsUnattended-upgrades is configured for automatic security patches:
# Check status
sudo unattended-upgrade --dry-run
# View logs
sudo cat /var/log/unattended-upgrades/unattended-upgrades.logNote: Automatic reboots are disabled. Monitor for pending reboots:
cat /var/run/reboot-required 2>/dev/null || echo "No reboot required"Run these checks after installation and onboarding. Interface names, IP addresses, packet counters, and process IDs vary by host; compare the stated healthy result rather than expecting byte-for-byte output.
sudo ufw status verboseExpected: Status: active, with incoming and routed traffic denied by default. The default rules allow 22/tcp for SSH and, when Tailscale is enabled, 41641/udp; IPv6-enabled hosts may show matching (v6) entries.
sudo fail2ban-client status
sudo fail2ban-client status sshdExpected: the first command lists sshd under Jail list; the second reports the jail as active and shows its failure and ban counters. Zero failures or bans is healthy.
sudo ss -tlnpExpected: SSH listens on the configured public address or 0.0.0.0; OpenClaw and its supporting services listen on 127.0.0.1. No OpenClaw or Docker service should listen on 0.0.0.0.
sudo iptables -L DOCKER-USER -n -vExpected: the chain includes accepts for established traffic and loopback traffic, followed by a drop rule for traffic arriving on the server's default external interface. Packet counters may be zero before traffic reaches the chain.
From another machine, scan the server:
nmap -p- YOUR_SERVER_IPExpected: only the configured SSH TCP port is open in the default configuration. Tailscale uses UDP port 41641, so it does not appear in this TCP scan.
Then publish a temporary container port:
sudo docker run -d -p 80:80 --name test-nginx nginx
curl --connect-timeout 5 http://YOUR_SERVER_IP:80
curl --fail http://localhost:80
sudo docker rm -f test-nginxExpected: the external request fails or times out, while the localhost request returns the nginx welcome page. Remove the test container even if either request produces an unexpected result.
sudo tailscale statusWhen Tailscale is enabled, expected: the server has a 100.x.x.x Tailscale address and appears in the peer table. Logged out or Stopped means sudo tailscale up still needs to be completed. Skip this check when tailscale_enabled is false.
sudo systemctl status unattended-upgradesExpected: the unit is loaded and active. Use sudo unattended-upgrade --dry-run --debug if the service is inactive or reports errors.
OpenClaw's web interface (port 3000) is bound to localhost. Access it via:
-
SSH tunnel:
ssh -L 3000:localhost:3000 user@server # Then browse to http://localhost:3000 -
Tailscale (recommended):
# On server: already done by playbook sudo tailscale up # From your machine: # Browse to http://TAILSCALE_IP:3000
Internet → UFW (SSH only) → fail2ban → DOCKER-USER Chain → DROP
Container → NAT → Internet (outbound allowed)
- macOS firewall configuration is basic (Application Firewall only)
- No fail2ban equivalent on macOS
- Consider using Little Snitch or similar for enhanced macOS security
- Docker IPv6 is disabled by default (
ip6tables: falsein daemon.json) - If your network uses IPv6, review and test firewall rules accordingly
- The
curl | bashinstallation pattern has inherent risks - For high-security environments, clone the repository and audit before running
- Consider using
--checkmode first:ansible-playbook playbook.yml --check
After installation, verify:
-
sudo ufw statusshows only SSH and Tailscale allowed -
sudo fail2ban-client status sshdshows jail active -
sudo iptables -L DOCKER-USER -nshows DROP rule -
nmap -p- YOUR_IPfrom external shows only port 22 -
docker run -p 80:80 nginx+curl YOUR_IP:80times out - Tailscale access works for web UI
If you discover a security vulnerability, please report it privately:
- OpenClaw: https://github.com/openclaw/openclaw/security
- This installer: https://github.com/openclaw/openclaw-ansible/security