diff --git a/AGENTS.md b/AGENTS.md index fa05908..8ae7d25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. @@ -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 diff --git a/README.md b/README.md index 253d8f3..d3603e0 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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 diff --git a/docs/security.md b/docs/security.md index cb60464..7dce18d 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 @@ -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: @@ -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: @@ -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 @@ -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 @@ -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 diff --git a/roles/clawdbot/handlers/main.yml b/roles/clawdbot/handlers/main.yml index 37b5807..facecbe 100644 --- a/roles/clawdbot/handlers/main.yml +++ b/roles/clawdbot/handlers/main.yml @@ -3,3 +3,8 @@ ansible.builtin.systemd: name: docker state: restarted + +- name: Restart fail2ban + ansible.builtin.systemd: + name: fail2ban + state: restarted diff --git a/roles/clawdbot/tasks/firewall-linux.yml b/roles/clawdbot/tasks/firewall-linux.yml index 1a7d94a..b33cc6e 100644 --- a/roles/clawdbot/tasks/firewall-linux.yml +++ b/roles/clawdbot/tasks/firewall-linux.yml @@ -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 diff --git a/roles/clawdbot/tasks/user.yml b/roles/clawdbot/tasks/user.yml index f1ecf86..95fe38a 100644 --- a/roles/clawdbot/tasks/user.yml +++ b/roles/clawdbot/tasks/user.yml @@ -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 diff --git a/roles/clawdbot/templates/clawdbot-host.service.j2 b/roles/clawdbot/templates/clawdbot-host.service.j2 index 850cca4..109a333 100644 --- a/roles/clawdbot/templates/clawdbot-host.service.j2 +++ b/roles/clawdbot/templates/clawdbot-host.service.j2 @@ -1,5 +1,5 @@ [Unit] -Description=Clawdbot WhatsApp Gateway +Description=Clawdbot AI Gateway After=network.target docker.service Requires=docker.service @@ -13,10 +13,10 @@ WorkingDirectory={{ clawdbot_home }} Environment="PATH={{ clawdbot_home }}/.local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin" Environment="PNPM_HOME={{ clawdbot_home }}/.local/share/pnpm" Environment="HOME={{ clawdbot_home }}" -Environment="XDG_RUNTIME_DIR=/run/user/1000" +Environment="XDG_RUNTIME_DIR=/run/user/{{ clawdbot_uid_value | default('1000') }}" # DBus session bus -Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" +Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ clawdbot_uid_value | default('1000') }}/bus" # Start command ExecStart={{ clawdbot_home }}/.local/bin/clawdbot gateway @@ -31,6 +31,7 @@ PrivateTmp=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths={{ clawdbot_home }}/.clawdbot +ReadWritePaths={{ clawdbot_home }}/.local # Logging StandardOutput=journal