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