--- # ------------------------------------------------- # Deploy Fail2Ban integrated with Proxmox Firewall # ------------------------------------------------- ################################################# # Detect cluster ################################################# - name: fail2ban | Detect Proxmox cluster ansible.builtin.stat: path: /etc/pve/corosync.conf register: cluster_status - name: fail2ban | Set cluster fact ansible.builtin.set_fact: pve_clustered: "{{ cluster_status.stat.exists }}" ################################################# # Determine Correct Firewall File ################################################# - name: fail2ban | Get Proxmox node name ansible.builtin.command: hostname register: pve_node changed_when: false - name: fail2ban | Set firewall config path ansible.builtin.set_fact: pve_firewall_config: >- {{ '/etc/pve/firewall/cluster.fw' if pve_clustered else '/etc/pve/firewall/' + pve_node.stdout + '.fw' }} ################################################# # Detect firewall configuration ################################################# - name: fail2ban | Check firewall config exists ansible.builtin.stat: path: "{{ pve_firewall_config }}" register: fw_stat - name: fail2ban | Read firewall config ansible.builtin.slurp: src: "{{ pve_firewall_config }}" register: fw_content when: fw_stat.stat.exists - name: fail2ban | Determine if firewall enabled ansible.builtin.set_fact: pve_firewall_enabled: >- {{ fw_stat.stat.exists and (fw_content.content | b64decode) is search('enable:\s*1') }} - name: fail2ban | Warn if firewall not enabled ansible.builtin.debug: msg: > WARNING: Proxmox firewall is disabled in configuration. Fail2Ban will not actively block traffic. when: not pve_firewall_enabled ################################################# # Validate firewall runtime state ################################################# - name: fail2ban | Check firewall runtime status ansible.builtin.command: pve-firewall status register: pve_fw_status changed_when: false failed_when: false - name: fail2ban | Abort if firewall daemon not running ansible.builtin.fail: msg: > Proxmox firewall service is not running. Run: systemctl enable --now pve-firewall when: pve_fw_status.rc != 0 ################################################# # Corosync safety validation ################################################# - name: fail2ban | Validate corosync firewall rules ansible.builtin.command: pve-firewall compile register: compiled_fw changed_when: false failed_when: false when: cluster_status.stat.exists - name: fail2ban | Fail if corosync ports are being dropped ansible.builtin.fail: msg: > Firewall configuration appears to affect Corosync ports (5404/5405). Refusing to continue to prevent cluster outage. when: - cluster_status.stat.exists - compiled_fw.stdout is search('5404.*DROP|5405.*DROP') ################################################# # Install Fail2Ban ################################################# - name: fail2ban | Install fail2ban ansible.builtin.apt: name: fail2ban state: present update_cache: true ################################################# # Create Proxmox firewall IPSet ################################################# - name: fail2ban | Ensure firewall directory exists ansible.builtin.file: path: /etc/pve/firewall state: directory - name: fail2ban | Add Fail2Ban IPSet to cluster firewall ansible.builtin.blockinfile: path: "{{ pve_firewall_config }}" marker: "# {mark} ANSIBLE FAIL2BAN IPSET" block: | [IPSET {{ f2b_ipset_name }}] comment: Fail2Ban dynamic blacklist create: true - name: fail2ban | Ensure RULES section exists ansible.builtin.blockinfile: path: "{{ pve_firewall_config }}" marker: "# {mark} ANSIBLE RULES HEADER" block: | [RULES] - name: fail2ban | Add drop rule for Fail2Ban IPSet ansible.builtin.blockinfile: path: "{{ pve_firewall_config }}" marker: "# {mark} ANSIBLE FAIL2BAN RULE" block: | IN DROP -source +{{ f2b_ipset_name }} - name: fail2ban | Extract corosync ring0 address ansible.builtin.shell: grep ring0_addr /etc/pve/corosync.conf | awk '{print $2}' register: corosync_ip changed_when: false when: cluster_status.stat.exists # Then automatically whitelist it in Fail2Ban: # ignoreip = 127.0.0.1/8 {{ corosync_ip.stdout }} ################################################# # Create Fail2Ban Proxmox action ################################################# - name: fail2ban | Create Proxmox firewall action ansible.builtin.copy: dest: /etc/fail2ban/action.d/proxmox-fw.conf mode: '0644' content: | [Definition] actionstart = actionstop = actioncheck = actionban = /usr/sbin/pve-firewall ipset add {{ f2b_ipset_name }} actionunban = /usr/sbin/pve-firewall ipset del {{ f2b_ipset_name }} ################################################# # Configure AD-aware jail ################################################# - name: fail2ban | Ensure jail.d exists ansible.builtin.file: path: /etc/fail2ban/jail.d state: directory mode: '0755' - name: fail2ban | Configure Fail2Ban jails ansible.builtin.copy: dest: /etc/fail2ban/jail.d/proxmox.conf mode: '0644' content: | [DEFAULT] bantime = {{ f2b_bantime }} findtime = {{ f2b_findtime }} maxretry = {{ f2b_maxretry }} bantime.increment = {{ f2b_bantime_increment }} bantime.factor = {{ f2b_bantime_factor }} bantime.max = {{ f2b_bantime_max }} backend = systemd banaction = proxmox-fw ignoreip = 127.0.0.1/8{% if pve_clustered %} {{ corosync_ip.stdout }}{% endif %} 192.168.2.0/24 ################################################# # SSH ################################################# [sshd] enabled = true journalmatch = _SYSTEMD_UNIT=sshd.service ################################################# # Proxmox GUI + AD authentication ################################################# [proxmox-auth] enabled = true port = https,8006 filter = proxmox-auth logpath = /var/log/pveproxy/access.log ################################################# # Progressive escalation (recidive) ################################################# [recidive] enabled = true filter = recidive logpath = /var/log/fail2ban.log bantime = {{ f2b_recidive_bantime }} findtime = {{ f2b_recidive_findtime }} maxretry = {{ f2b_recidive_maxretry }} banaction = proxmox-fw notify: - Restart fail2ban ################################################# # AD / Winbind filter ################################################# - name: fail2ban | Create AD-aware filter ansible.builtin.copy: dest: /etc/fail2ban/filter.d/proxmox-auth.conf mode: '0644' content: | [Definition] failregex = authentication failure; rhost= pam_unix\(sshd:auth\): authentication failure;.*rhost= winbind.*authentication for user.*from failed ignoreregex = notify: - Restart fail2ban ################################################# # Enable services ################################################# - name: fail2ban | Enable fail2ban ansible.builtin.systemd: name: fail2ban enabled: true state: started - name: fail2ban | Reload Proxmox firewall ansible.builtin.command: pve-firewall reload when: fw_stat.changed or "'ANSIBLE FAIL2BAN' in fw_content.content | default('')" changed_when: false ################################################# # List banned IPs cluster-wide ################################################# - name: fail2ban | Get banned IPs from Proxmox IPSet ansible.builtin.command: pve-firewall ipset list {{ f2b_ipset_name }} register: banned_ips changed_when: false failed_when: false - name: fail2ban | Show banned IPs ansible.builtin.debug: msg: > Current banned IPs (cluster-wide): {{ banned_ips.stdout_lines | default([]) }} ################################################# # Manual unban ################################################# - name: fail2ban | Unban specific IP ansible.builtin.command: > pve-firewall ipset del {{ f2b_ipset_name }} {{ f2b_unban_ip }} when: f2b_unban_ip | length > 0 register: unban_result changed_when: "'removed' in unban_result.stdout or unban_result.rc == 0" failed_when: false - name: fail2ban | Report unban result ansible.builtin.debug: msg: "Unbanned IP {{ f2b_unban_ip }}" when: f2b_unban_ip | length > 0