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.
315 lines
9.4 KiB
YAML
315 lines
9.4 KiB
YAML
---
|
|
# -------------------------------------------------
|
|
# Deploy Fail2Ban integrated with Proxmox Firewall
|
|
# -------------------------------------------------
|
|
|
|
#################################################
|
|
# Detect Proxmox
|
|
#################################################
|
|
|
|
- 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: clustered
|
|
when: pmxcfs_running.stat.exists | default(false)
|
|
|
|
- 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=<HOST> user=.* msg=.*
|
|
ignoreregex =
|
|
journalmatch = _SYSTEMD_UNIT=pvedaemon.service
|
|
owner: root
|
|
group: root
|
|
mode: '0644'
|
|
notify:
|
|
- Reload fail2ban
|
|
|
|
#################################################
|
|
# Determine Correct Firewall File
|
|
#################################################
|
|
|
|
- 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 clustered.stat.exists
|
|
else '/etc/pve/nodes/' + pve_node + '.fw'
|
|
}}
|
|
when: pve_installed.stat.exists | default(false)
|
|
|
|
#################################################
|
|
# Detect firewall configuration
|
|
#################################################
|
|
|
|
- name: fail2ban | Check firewall config exists
|
|
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 | default(false)
|
|
|
|
- name: fail2ban | Determine if firewall enabled
|
|
ansible.builtin.set_fact:
|
|
pve_firewall_enabled: >-
|
|
{{
|
|
(fw_stat.stat.exists | default(false)) and
|
|
(
|
|
(fw_content.content | default('') | 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
|
|
when: pmxcfs_running.stat.exists | default(false)
|
|
|
|
- name: fail2ban | Abort if firewall daemon not running
|
|
ansible.builtin.debug:
|
|
msg: >
|
|
Proxmox firewall service is not running.
|
|
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
|
|
#################################################
|
|
|
|
- name: fail2ban | Validate corosync firewall rules
|
|
ansible.builtin.command: pve-firewall compile
|
|
register: compiled_fw
|
|
changed_when: false
|
|
failed_when: compiled_fw.rc != 0
|
|
when: clustered.stat.exists | default(false)
|
|
|
|
- name: fail2ban | Fail if corosync ports are being dropped
|
|
ansible.builtin.debug:
|
|
msg: >
|
|
Firewall configuration appears to affect Corosync ports (5404/5405).
|
|
Refusing to continue to prevent cluster outage.
|
|
when:
|
|
- clustered.stat.exists | default(false)
|
|
- compiled_fw.stdout is search('5404.*DROP|5405.*DROP')
|
|
|
|
#################################################
|
|
# Deploy cluster-aware Fail2Ban 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 <ip> -log nolog
|
|
|
|
actionban = \
|
|
if [ -f <fwfile> ]; then \
|
|
grep -qF "<rule>" <fwfile> || echo "<rule>" >> <fwfile>; \
|
|
pve-firewall compile >/dev/null 2>&1 || true; \
|
|
fi
|
|
|
|
actionunban = \
|
|
if [ -f <fwfile> ]; then \
|
|
sed -i "\|<rule>|d" <fwfile>; \
|
|
pve-firewall compile >/dev/null 2>&1 || true; \
|
|
fi
|
|
|
|
actionstart =
|
|
actionstop =
|
|
when:
|
|
- clustered.stat.exists | default(false)
|
|
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 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
|
|
|
|
- name: fail2ban | Report unban result
|
|
ansible.builtin.debug:
|
|
msg: "Unbanned IP {{ f2b_unban_ip }}"
|
|
when: f2b_unban_ip | length > 0
|