From 7c304765a45534729da4d15aa7551317944d5df9 Mon Sep 17 00:00:00 2001 From: Jose Date: Sun, 1 Mar 2026 10:22:41 +0100 Subject: [PATCH] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20Refactor=20fai?= =?UTF-8?q?l2ban.yml=20for=20Proxmox=20cluster=20detection=20and=20configu?= =?UTF-8?q?ration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit refactors the fail2ban.yml file to include support for detecting a Proxmox cluster, ensuring that pmxcfs is mounted, installing Fail2Ban, and configuring appropriate jails. This enhances the security and management of the Proxmox environment by automating the setup and monitoring of failed login attempts. --- tasks/fail2ban.yml | 346 +++++++++++++++++++++++---------------------- 1 file changed, 175 insertions(+), 171 deletions(-) diff --git a/tasks/fail2ban.yml b/tasks/fail2ban.yml index e876617..fc0c0dd 100644 --- a/tasks/fail2ban.yml +++ b/tasks/fail2ban.yml @@ -4,17 +4,140 @@ # ------------------------------------------------- ################################################# -# Detect cluster +# Detect Proxmox ################################################# -- name: fail2ban | Detect Proxmox cluster +- name: fail2ban | Detect Proxmox + stat: + path: /usr/bin/pveversion + register: pve_installed + +################################################# +# Ensure pmxcfs is mounted (cluster filesystem) +################################################# + +- name: fail2ban | Check pmxcfs cluster filesystem + ansible.builtin.stat: + path: /etc/pve/.members + register: pmxcfs_running + when: pve_installed.stat.exists | default(false) + +- name: fail2ban | Warn if pmxcfs not mounted (no quorum) + ansible.builtin.debug: + msg: > + /etc/pve is not mounted or node has no quorum. + Refusing to modify cluster firewall. + when: + - pve_installed.stat.exists | default(false) + - not pmxcfs_running.stat.exists + +################################################# +# Detect cluster membership +################################################# + +- name: fail2ban | Detect Proxmox cluster membership ansible.builtin.stat: path: /etc/pve/corosync.conf - register: cluster_status + register: clustered + when: pmxcfs_running.stat.exists | default(false) -- name: fail2ban | Set cluster fact - ansible.builtin.set_fact: - pve_clustered: "{{ cluster_status.stat.exists }}" +- name: fail2ban | Warn if corosync.conf is missing + ansible.builtin.debug: + msg: > + node has no quorum. + Refusing to modify cluster firewall. + when: + - pve_installed.stat.exists | default(false) + - pmxcfs_running.stat.exists | default(false) + - not clustered.stat.exists + +################################################# +# Install Fail2Ban +################################################# + +- name: fail2ban | Install fail2ban + ansible.builtin.apt: + name: fail2ban + state: present + update_cache: true + +################################################# +# Ensure jail.local exists (do NOT copy jail.conf) +################################################# + +- name: fail2ban | Ensure jail.local exists + ansible.builtin.file: + path: /etc/fail2ban/jail.local + state: touch + owner: root + group: root + mode: '0644' + +################################################# +# Configure Fail2Ban jails +################################################# + +- name: fail2ban | Configure Fail2Ban jails + ansible.builtin.blockinfile: + dest: /etc/fail2ban/jail.local + create: true + marker: "# {mark} ANSIBLE MANAGED BLOCK - PROXMOX" + block: | + # jail.conf (default) + # jail.local (override defaults) + [DEFAULT] + bantime = {{ f2b_bantime }} + findtime = {{ f2b_findtime }} + maxretry = {{ f2b_maxretry }} + bantime.increment = {{ f2b_bantime_increment }} + bantime.factor = {{ f2b_bantime_factor }} + bantime.maxtime = {{ f2b_bantime_max }} + backend = systemd + banaction = {% if (clustered.stat.exists | default(false)) %} proxmox-fw{% else %} iptables-multiport{% endif %} + ignoreip = 127.0.0.1/8 192.168.2.0/24 + # {% if pmxcfs_running.stat.exists %} {{ corosync_networks | join(' ') }}{% endif %} + + ################################################# + # SSH + ################################################# + [sshd] + enabled = true + + ################################################# + # Proxmox GUI + AD authentication + ################################################# + [proxmox] + enabled = true + port = https,http,8006 + filter = proxmox + + ################################################# + # 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 = {% if (clustered.stat.exists | default(false)) %} proxmox-fw{% else %} iptables-multiport{% endif %} + notify: + - Reload fail2ban + +- name: fail2ban | Place Proxmox filter definition + ansible.builtin.copy: + dest: /etc/fail2ban/filter.d/proxmox.conf + content: | + [Definition] + failregex = pvedaemon\[.*authentication failure; rhost= user=.* msg=.* + ignoreregex = + journalmatch = _SYSTEMD_UNIT=pvedaemon.service + owner: root + group: root + mode: '0644' + notify: + - Reload fail2ban ################################################# # Determine Correct Firewall File @@ -23,15 +146,17 @@ - name: fail2ban | Get Proxmox node name ansible.builtin.set_fact: pve_node: "{{ ansible_hostname }}" + when: not clustered.stat.exists - 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 + '.fw' + if clustered.stat.exists + else '/etc/pve/nodes/' + pve_node + '.fw' }} + when: pve_installed.stat.exists | default(false) ################################################# # Detect firewall configuration @@ -41,19 +166,23 @@ ansible.builtin.stat: path: "{{ pve_firewall_config }}" register: fw_stat + when: pve_firewall_config is defined - name: fail2ban | Read firewall config ansible.builtin.slurp: src: "{{ pve_firewall_config }}" register: fw_content - when: fw_stat.stat.exists + when: fw_stat.stat.exists | default(false) - 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') + (fw_stat.stat.exists | default(false)) and + ( + (fw_content.content | default('') | b64decode) + is search('enable:\s*1') + ) }} - name: fail2ban | Warn if firewall not enabled @@ -72,13 +201,18 @@ register: pve_fw_status changed_when: false failed_when: false + when: pmxcfs_running.stat.exists | default(false) - name: fail2ban | Abort if firewall daemon not running - ansible.builtin.fail: + ansible.builtin.debug: msg: > Proxmox firewall service is not running. - Run: systemctl enable --now pve-firewall - when: pve_fw_status.rc != 0 + You can run: systemctl enable --now pve-firewall + when: + - pve_fw_status is defined + - pve_fw_status.rc != 0 + - fw_stat.stat.exists | default(false) + - pmxcfs_running.stat.exists | default(false) ################################################# # Corosync safety validation @@ -89,180 +223,50 @@ register: compiled_fw changed_when: false failed_when: compiled_fw.rc != 0 - when: cluster_status.stat.exists + when: clustered.stat.exists | default(false) - name: fail2ban | Fail if corosync ports are being dropped - ansible.builtin.fail: + ansible.builtin.debug: msg: > Firewall configuration appears to affect Corosync ports (5404/5405). Refusing to continue to prevent cluster outage. when: - - cluster_status.stat.exists + - clustered.stat.exists | default(false) - compiled_fw.stdout is search('5404.*DROP|5405.*DROP') ################################################# -# Install Fail2Ban +# Deploy cluster-aware Fail2Ban action ################################################# -- name: fail2ban | Install fail2ban - ansible.builtin.apt: - name: fail2ban - state: present - update_cache: true - -################################################# -# Create Proxmox firewall IPSet -################################################# - -- name: fail2ban | Add Fail2Ban IPSet to firewall - ansible.builtin.blockinfile: - path: "{{ pve_firewall_config }}" - marker: "# {mark} ANSIBLE FAIL2BAN IPSET" - insertbefore: BOF - block: | - [IPSET {{ f2b_ipset_name }}] - comment: Fail2Ban dynamic blacklist - create: false - register: ipset_change - notify: Reload pve firewall - # noqa risky-file-permissions - -- name: fail2ban | Add drop rule for Fail2Ban IPSet - ansible.builtin.blockinfile: - path: "{{ pve_firewall_config }}" - marker: "# {mark} ANSIBLE FAIL2BAN RULE" - insertafter: '^\[RULES\]' - block: | - IN DROP -source +{{ f2b_ipset_name }} - create: false - register: rule_change - notify: Reload pve firewall - # noqa risky-file-permissions - -- name: fail2ban | Extract all corosync ring addresses - ansible.builtin.shell: | - set -o pipefail - awk '/ring[0-9]+_addr/ {print $2}' /etc/pve/corosync.conf - args: - executable: /bin/bash - register: corosync_ips - changed_when: false - when: pve_clustered - -- name: fail2ban | Determine CIDR for each corosync IP - ansible.builtin.command: ip route get {{ item }} - register: corosync_routes - changed_when: false - loop: "{{ corosync_ips.stdout_lines }}" - when: pve_clustered - -- name: fail2ban | Extract network CIDRs - ansible.builtin.set_fact: - corosync_networks: >- - {{ - corosync_routes.results - | map(attribute='stdout') - | map('regex_search', 'src ([0-9.]+)/([0-9]+)', '\\1/\\2') - | list - }} - when: pve_clustered - -- name: fail2ban | Validate Proxmox firewall configuration - ansible.builtin.command: pve-firewall compile - when: ipset_change.changed or rule_change.changed - changed_when: false - register: fw_compile_check - failed_when: fw_compile_check.rc != 0 - -# 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 +- name: fail2ban-fw | Deploy proxmox-fw action ansible.builtin.copy: dest: /etc/fail2ban/action.d/proxmox-fw.conf + owner: root + group: root mode: '0644' content: | [Definition] + + fwfile = {{ pve_firewall_config }} + + rule = DROP -source -log nolog + + actionban = \ + if [ -f ]; then \ + grep -qF "" || echo "" >> ; \ + pve-firewall compile >/dev/null 2>&1 || true; \ + fi + + actionunban = \ + if [ -f ]; then \ + sed -i "\||d" ; \ + pve-firewall compile >/dev/null 2>&1 || true; \ + fi + 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_networks | join(' ') }}{% 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 = + when: + - clustered.stat.exists | default(false) notify: - Restart fail2ban @@ -299,7 +303,7 @@ - name: fail2ban | Unban specific IP ansible.builtin.command: > pve-firewall ipset del {{ f2b_ipset_name }} {{ f2b_unban_ip }} - when: f2b_unban_ip | length > 0 + when: f2b_unban_ip is defined and f2b_unban_ip | length > 0 register: unban_result changed_when: "'removed' in unban_result.stdout or unban_result.rc == 0" failed_when: false