Files
ansible_role_proxmox_provision/tasks/fail2ban.yml
Jose 80b3b82bf6
Some checks failed
ansible-lint / Ansible Lint (push) Failing after 6s
Gitleaks Scan / gitleaks (push) Successful in 5s
Markdown Lint / markdown-lint (push) Successful in 5s
refactor ♻️: Refactor fail2ban tasks for better IPSet management
This refactoring removes redundant 'blockinfile' and 'reload' commands in fail2ban tasks, ensuring that IPSet and drop rules are correctly placed. A new handler has been added to reload the PVE firewall after a fail2ban restart.
2026-02-24 18:46:12 +01:00

285 lines
8.4 KiB
YAML

---
# -------------------------------------------------
# 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 | 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
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
notify: Reload pve firewall
# noqa risky-file-permissions
- name: fail2ban | Extract corosync ring0 address
ansible.builtin.shell: |
set -o pipefail
grep ring0_addr /etc/pve/corosync.conf | awk '{print $2}'
args:
executable: /bin/bash
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 }} <ip>
actionunban = /usr/sbin/pve-firewall ipset del {{ f2b_ipset_name }} <ip>
#################################################
# 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=<HOST>
pam_unix\(sshd:auth\): authentication failure;.*rhost=<HOST>
winbind.*authentication for user.*from <HOST> 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