Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ requirements.yml # Ansible Galaxy collections
### Why UFW + DOCKER-USER?
Docker bypasses UFW by default. DOCKER-USER chain is evaluated first, allowing us to block before Docker sees the traffic.

### Why Fail2ban?
SSH is exposed to the internet. Fail2ban automatically bans IPs after 5 failed attempts for 1 hour.

### Why Unattended-Upgrades?
Security patches should be applied promptly. Automatic security-only updates reduce vulnerability windows.

### Why Scoped Sudo?
The clawdbot user only needs to manage its own service and Tailscale. Full root access would be dangerous if the app is compromised.

### Why Localhost Binding?
Defense in depth. If DOCKER-USER fails, localhost binding prevents external access.

Expand All @@ -143,6 +152,11 @@ Least privilege. Limits damage if container is compromised.
### Why Systemd?
Clean lifecycle, auto-start, logging integration.

### Known Limitations
- **macOS**: Incomplete support (no launchd, basic firewall). Test thoroughly.
- **IPv6**: Disabled in Docker. Review if your network uses IPv6.
- **curl | bash**: Inherent risks. For production, clone and audit first.

## Making Changes

### Adding a New Task
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ Automated, hardened installation of [Clawdbot](https://github.com/clawdbot/clawd
## Features

- 🔒 **Firewall-first**: UFW (Linux) + Application Firewall (macOS) + Docker isolation
- 🛡️ **Fail2ban**: SSH brute-force protection out of the box
- 🔄 **Auto-updates**: Automatic security patches via unattended-upgrades
- 🔐 **Tailscale VPN**: Secure remote access without exposing services
- 🍺 **Homebrew**: Package manager for both Linux and macOS
- 🐳 **Docker**: Docker CE (Linux) / Docker Desktop (macOS)
- 🛡️ **Multi-OS Support**: Debian, Ubuntu, and macOS
- 🌐 **Multi-OS Support**: Debian, Ubuntu, and macOS
- 🚀 **One-command install**: Complete setup in minutes
- 🔧 **Auto-configuration**: DBus, systemd, environment setup
- 📦 **pnpm installation**: Uses `pnpm install -g clawdbot@latest`
Expand Down Expand Up @@ -113,13 +115,27 @@ Enable with: `-e clawdbot_install_mode=development`
## Security

- **Public ports**: SSH (22), Tailscale (41641/udp) only
- **Docker available**: For Clawdbot sandboxes (isolated execution)
- **Fail2ban**: SSH brute-force protection (5 attempts → 1 hour ban)
- **Automatic updates**: Security patches via unattended-upgrades
- **Docker isolation**: Containers can't expose ports externally (DOCKER-USER chain)
- **Non-root**: Clawdbot runs as unprivileged user
- **Systemd hardening**: NoNewPrivileges, PrivateTmp
- **Scoped sudo**: Limited to service management (not full root)
- **Systemd hardening**: NoNewPrivileges, PrivateTmp, ProtectSystem

Verify: `nmap -p- YOUR_SERVER_IP` should show only port 22 open.

### Security Note

For high-security environments, audit before running:

```bash
git clone https://github.com/openclaw/clawdbot-ansible.git
cd clawdbot-ansible
# Review playbook.yml and roles/
ansible-playbook playbook.yml --check --diff # Dry run
ansible-playbook playbook.yml --ask-become-pass
```

## Documentation

- [Configuration Guide](docs/configuration.md) - All configuration options
Expand Down
114 changes: 106 additions & 8 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
---
title: Security Architecture
description: Firewall configuration and Docker isolation details
description: Firewall configuration, Docker isolation, and security hardening details
---

# Security Architecture

## Overview

This playbook implements a 4-layer defense strategy to ensure only SSH (port 22) is accessible from the internet.
This playbook implements a multi-layer defense strategy to secure Clawdbot installations.

## Layer 1: UFW Firewall
## Security Layers

### Layer 1: UFW Firewall

```bash
# Default policies
Expand All @@ -22,7 +24,24 @@ SSH (22/tcp): ALLOW
Tailscale (41641/udp): ALLOW
```

## Layer 2: DOCKER-USER Chain
### Layer 2: Fail2ban (SSH Protection)

Automatic protection against SSH brute-force attacks:

```bash
# 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_ADDRESS
```

### Layer 3: DOCKER-USER Chain

Custom iptables chain that prevents Docker from bypassing UFW:

Expand All @@ -37,7 +56,7 @@ COMMIT

**Result**: Even `docker run -p 80:80 nginx` won't expose port 80 externally.

## Layer 3: Localhost-Only Binding
### Layer 4: Localhost-Only Binding

All container ports bind to 127.0.0.1:

Expand All @@ -46,16 +65,58 @@ ports:
- "127.0.0.1:3000:3000"
```

## Layer 4: Non-Root Container
### Layer 5: Non-Root Container

Container processes run as unprivileged `clawdbot` user.

### Layer 6: Systemd Hardening

The clawdbot service runs with security restrictions:

- `NoNewPrivileges=true` - Prevents privilege escalation
- `PrivateTmp=true` - Isolated /tmp directory
- `ProtectSystem=strict` - Read-only system directories
- `ProtectHome=read-only` - Limited home directory access
- `ReadWritePaths` - Only ~/.clawdbot is writable

### Layer 7: Scoped Sudo Access

The clawdbot user has limited sudo permissions (not full root):

```bash
# Allowed commands only:
- systemctl start/stop/restart/status clawdbot
- systemctl daemon-reload
- tailscale commands
- journalctl for clawdbot logs
```

### Layer 8: Automatic Security Updates

Unattended-upgrades is configured for automatic security patches:

```bash
# Check status
sudo unattended-upgrade --dry-run

# View logs
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
```

**Note**: Automatic reboots are disabled. Monitor for pending reboots:
```bash
cat /var/run/reboot-required 2>/dev/null || echo "No reboot required"
```

## Verification

```bash
# Check firewall
sudo ufw status verbose

# Check fail2ban
sudo fail2ban-client status

# Check Tailscale status
sudo tailscale status

Expand All @@ -66,9 +127,13 @@ sudo iptables -L DOCKER-USER -n -v
nmap -p- YOUR_SERVER_IP

# Test container isolation
sudo docker run -d -p 80:80 nginx
sudo docker run -d -p 80:80 --name test-nginx nginx
curl http://YOUR_SERVER_IP:80 # Should fail/timeout
curl http://localhost:80 # Should work
sudo docker rm -f test-nginx

# Check unattended-upgrades
sudo systemctl status unattended-upgrades
```

## Tailscale Access
Expand All @@ -93,6 +158,39 @@ Clawdbot's web interface (port 3000) is bound to localhost. Access it via:
## Network Flow

```
Internet → UFW (SSH only) → DOCKER-USER Chain → DROP (unless localhost/established)
Internet → UFW (SSH only) → fail2ban → DOCKER-USER Chain → DROP
Container → NAT → Internet (outbound allowed)
```

## Known Limitations

### macOS Support
- macOS firewall configuration is basic (Application Firewall only)
- No fail2ban equivalent on macOS
- Consider using Little Snitch or similar for enhanced macOS security

### IPv6
- Docker IPv6 is disabled by default (`ip6tables: false` in daemon.json)
- If your network uses IPv6, review and test firewall rules accordingly

### Installation Script
- The `curl | bash` installation pattern has inherent risks
- For high-security environments, clone the repository and audit before running
- Consider using `--check` mode first: `ansible-playbook playbook.yml --check`

## Security Checklist

After installation, verify:

- [ ] `sudo ufw status` shows only SSH and Tailscale allowed
- [ ] `sudo fail2ban-client status sshd` shows jail active
- [ ] `sudo iptables -L DOCKER-USER -n` shows DROP rule
- [ ] `nmap -p- YOUR_IP` from external shows only port 22
- [ ] `docker run -p 80:80 nginx` + `curl YOUR_IP:80` times out
- [ ] Tailscale access works for web UI

## Reporting Security Issues

If you discover a security vulnerability, please report it privately:
- Clawdbot: https://github.com/clawdbot/clawdbot/security
- This installer: https://github.com/openclaw/clawdbot-ansible/security
5 changes: 5 additions & 0 deletions roles/clawdbot/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
ansible.builtin.systemd:
name: docker
state: restarted

- name: Restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
76 changes: 75 additions & 1 deletion roles/clawdbot/tasks/firewall-linux.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,80 @@
---
# Linux-specific firewall configuration (UFW)
# Linux-specific firewall configuration (UFW) with security hardening

# Install and configure fail2ban for SSH brute-force protection
- name: Install fail2ban
ansible.builtin.apt:
name: fail2ban
state: present
update_cache: true

- name: Configure fail2ban for SSH protection
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
content: |
# Clawdbot security hardening - SSH protection
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
backend = systemd

[sshd]
enabled = true
port = ssh
filter = sshd
# logpath not needed - systemd backend reads from journal
notify: Restart fail2ban

- name: Enable and start fail2ban
ansible.builtin.systemd:
name: fail2ban
state: started
enabled: true

# Install and configure unattended-upgrades for automatic security updates
- name: Install unattended-upgrades
ansible.builtin.apt:
name:
- unattended-upgrades
- apt-listchanges
state: present

- name: Configure automatic security updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: '0644'
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

- name: Configure unattended-upgrades to only install security updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
owner: root
group: root
mode: '0644'
content: |
// Clawdbot security hardening - automatic security updates
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";

# UFW Firewall configuration
- name: Install UFW
ansible.builtin.apt:
name: ufw
Expand Down
41 changes: 38 additions & 3 deletions roles/clawdbot/tasks/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,50 @@
home: /home/clawdbot
state: present

- name: Add clawdbot user to sudoers with NOPASSWD
- name: Add clawdbot user to sudoers with scoped NOPASSWD
ansible.builtin.copy:
dest: /etc/sudoers.d/clawdbot
mode: '0440'
owner: root
group: root
content: |
# Allow clawdbot user to run sudo without password
clawdbot ALL=(ALL) NOPASSWD: ALL
# Clawdbot sudo permissions (scoped for security)
#
# SECURITY NOTE: These permissions are intentionally limited.
# If clawdbot is compromised, attackers can only:
# - Manage the clawdbot service
# - Run basic tailscale diagnostics
# - View clawdbot logs
#
# To grant full tailscale control (e.g., for self-healing VPN):
# clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale *
#
# To grant full sudo (NOT RECOMMENDED):
# clawdbot ALL=(ALL) NOPASSWD: ALL

# Service control - clawdbot service only
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl start clawdbot
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop clawdbot
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart clawdbot
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl status clawdbot
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable clawdbot
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable clawdbot
# daemon-reload affects all units (required after service file changes)
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload

# Tailscale - diagnostics + connect/disconnect
# NOTE: 'up' allows flags like --advertise-exit-node. For tighter control,
# remove 'up' and 'down' lines - operator must then manage VPN manually.
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale status
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale up *
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale down
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale ip *
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale version
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale ping *
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/tailscale whois *

# Journal access - clawdbot logs only
clawdbot ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u clawdbot *
validate: /usr/sbin/visudo -cf %s

- name: Set clawdbot user as primary user for installation
Expand Down Expand Up @@ -66,7 +101,7 @@
mode: '0700'

- name: Add SSH authorized keys for clawdbot user
ansible.builtin.authorized_key:

Check failure on line 104 in roles/clawdbot/tasks/user.yml

View workflow job for this annotation

GitHub Actions / Ansible Lint

fqcn[canonical]

You should use canonical module name `ansible.posix.authorized_key` instead of `ansible.builtin.authorized_key`.
user: clawdbot
state: present
key: "{{ item }}"
Expand Down
Loading
Loading