diff --git a/README.md b/README.md index 0e32586..37a0742 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ | Logrotate protection | ✅ | ✅ | ✅ | | Powertop auto-tune | ✅ | ✅ | ✅ | | Utilities | ✅ | ✅ | ✅ | +| Fail2Ban Integration | ✅ | ✅ | ✅ | ## 📂 Directory Structure @@ -40,6 +41,7 @@ ansible_role_proxmox_provision/ ├── meta/ # Role metadata │ └── main.yml ├── tasks/ # Main role tasks +│ ├── fail2ban.yml # Fail2Ban integration tasks │ ├── logrotate.yml # logrotate setup │ ├── main.yml # Core tasks │ ├── powertop.yml # powertop setup @@ -69,6 +71,7 @@ proxmox_enable_powertop: true ## Logrotate proxmox_logrotate_maxsize: "100M" proxmox_logrotate_rotate: 7 +... ``` ## Example usage diff --git a/defaults/main.yml b/defaults/main.yml index 21c8068..041d1ba 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -47,3 +47,16 @@ journald_runtime_max_use: "100M" vm_dirty_ratio: 15 vm_dirty_background_ratio: 5 vm_swappiness: "{{ proxmox_swapiness }}" + +# Fail2ban settings +f2b_bantime: 1800 # 30 minutes +f2b_findtime: 600 +f2b_maxretry: 5 +f2b_recidive_bantime: 86400 # 24 hours +f2b_recidive_findtime: 86400 # 24 hours +f2b_recidive_maxretry: 3 +f2b_ipset_name: f2b-blacklist +f2b_bantime_increment: true +f2b_bantime_factor: 2 +f2b_bantime_max: 86400 +f2b_unban_ip: "" # ansible-playbook play.yml -e f2b_unban_ip=192.168.1.55 diff --git a/handlers/main.yml b/handlers/main.yml index 2ff08d9..600b186 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -31,3 +31,13 @@ - name: Reload systemd ansible.builtin.systemd: daemon_reload: true + +- name: Restart fail2ban + ansible.builtin.systemd: + name: fail2ban + state: reloaded + +- name: Reload pve firewall + ansible.builtin.command: pve-firewall reload + when: fw_compile_check.rc == 0 + changed_when: false diff --git a/tasks/fail2ban.yml b/tasks/fail2ban.yml new file mode 100644 index 0000000..8381c45 --- /dev/null +++ b/tasks/fail2ban.yml @@ -0,0 +1,310 @@ +--- +# ------------------------------------------------- +# 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.set_fact: + pve_node: "{{ ansible_hostname }}" + +- 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: fw_compile_check.rc != 0 + 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 | 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 + failed_when: fw_compile_check.rc != 0 + register: fw_compile_check + +# 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_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 = + notify: + - Restart fail2ban + +################################################# +# Enable services +################################################# + +- name: fail2ban | Enable fail2ban + ansible.builtin.systemd: + name: fail2ban + enabled: true + state: started + +################################################# +# 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