--- # 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: | # OpenClaw 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: | // OpenClaw 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 state: present update_cache: true - name: Set UFW default policies community.general.ufw: direction: "{{ item.direction }}" policy: "{{ item.policy }}" loop: - { direction: 'incoming', policy: 'deny' } - { direction: 'outgoing', policy: 'allow' } - { direction: 'routed', policy: 'deny' } - name: Allow SSH on port 22 community.general.ufw: rule: allow port: '22' proto: tcp comment: 'SSH' - name: Allow Tailscale UDP port 41641 community.general.ufw: rule: allow port: '41641' proto: udp comment: 'Tailscale' when: tailscale_enabled | bool - name: Get default network interface ansible.builtin.shell: cmd: | set -o pipefail ip route | grep default | awk '{print $5}' | head -n1 executable: /bin/bash register: default_interface changed_when: false - name: Validate default network interface was detected ansible.builtin.assert: that: - default_interface.stdout is defined - default_interface.stdout | length > 0 fail_msg: "Failed to detect default network interface. Cannot configure firewall rules safely." success_msg: "Default network interface detected: {{ default_interface.stdout }}" - name: Create UFW after.rules for Docker isolation ansible.builtin.blockinfile: path: /etc/ufw/after.rules marker: "# {mark} ANSIBLE MANAGED BLOCK - Docker isolation" insertbefore: "^COMMIT$" block: | # Docker port isolation - block all forwarded traffic by default :DOCKER-USER - [0:0] # Allow established connections -A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # Allow localhost -A DOCKER-USER -i lo -j ACCEPT # Block all other forwarded traffic to Docker containers from external interface -A DOCKER-USER -i {{ default_interface.stdout }} -j DROP create: false - name: Create Docker daemon config directory ansible.builtin.file: path: /etc/docker state: directory mode: '0755' - name: Configure Docker daemon to work with UFW ansible.builtin.template: src: daemon.json.j2 dest: /etc/docker/daemon.json owner: root group: root mode: '0644' notify: Restart docker - name: Enable UFW community.general.ufw: state: enabled - name: Reload UFW community.general.ufw: state: reloaded