refactor ♻️: Refactor fail2ban.yml for Proxmox cluster detection and configuration
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.
This commit is contained in:
@@ -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:
|
ansible.builtin.stat:
|
||||||
path: /etc/pve/corosync.conf
|
path: /etc/pve/corosync.conf
|
||||||
register: cluster_status
|
register: clustered
|
||||||
|
when: pmxcfs_running.stat.exists | default(false)
|
||||||
|
|
||||||
- name: fail2ban | Set cluster fact
|
- name: fail2ban | Warn if corosync.conf is missing
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.debug:
|
||||||
pve_clustered: "{{ cluster_status.stat.exists }}"
|
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
|
# Determine Correct Firewall File
|
||||||
@@ -23,15 +146,17 @@
|
|||||||
- name: fail2ban | Get Proxmox node name
|
- name: fail2ban | Get Proxmox node name
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
pve_node: "{{ ansible_hostname }}"
|
pve_node: "{{ ansible_hostname }}"
|
||||||
|
when: not clustered.stat.exists
|
||||||
|
|
||||||
- name: fail2ban | Set firewall config path
|
- name: fail2ban | Set firewall config path
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
pve_firewall_config: >-
|
pve_firewall_config: >-
|
||||||
{{
|
{{
|
||||||
'/etc/pve/firewall/cluster.fw'
|
'/etc/pve/firewall/cluster.fw'
|
||||||
if pve_clustered
|
if clustered.stat.exists
|
||||||
else '/etc/pve/firewall/' + pve_node + '.fw'
|
else '/etc/pve/nodes/' + pve_node + '.fw'
|
||||||
}}
|
}}
|
||||||
|
when: pve_installed.stat.exists | default(false)
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
# Detect firewall configuration
|
# Detect firewall configuration
|
||||||
@@ -41,19 +166,23 @@
|
|||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ pve_firewall_config }}"
|
path: "{{ pve_firewall_config }}"
|
||||||
register: fw_stat
|
register: fw_stat
|
||||||
|
when: pve_firewall_config is defined
|
||||||
|
|
||||||
- name: fail2ban | Read firewall config
|
- name: fail2ban | Read firewall config
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: "{{ pve_firewall_config }}"
|
src: "{{ pve_firewall_config }}"
|
||||||
register: fw_content
|
register: fw_content
|
||||||
when: fw_stat.stat.exists
|
when: fw_stat.stat.exists | default(false)
|
||||||
|
|
||||||
- name: fail2ban | Determine if firewall enabled
|
- name: fail2ban | Determine if firewall enabled
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
pve_firewall_enabled: >-
|
pve_firewall_enabled: >-
|
||||||
{{
|
{{
|
||||||
fw_stat.stat.exists and
|
(fw_stat.stat.exists | default(false)) and
|
||||||
(fw_content.content | b64decode) is search('enable:\s*1')
|
(
|
||||||
|
(fw_content.content | default('') | b64decode)
|
||||||
|
is search('enable:\s*1')
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- name: fail2ban | Warn if firewall not enabled
|
- name: fail2ban | Warn if firewall not enabled
|
||||||
@@ -72,13 +201,18 @@
|
|||||||
register: pve_fw_status
|
register: pve_fw_status
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
when: pmxcfs_running.stat.exists | default(false)
|
||||||
|
|
||||||
- name: fail2ban | Abort if firewall daemon not running
|
- name: fail2ban | Abort if firewall daemon not running
|
||||||
ansible.builtin.fail:
|
ansible.builtin.debug:
|
||||||
msg: >
|
msg: >
|
||||||
Proxmox firewall service is not running.
|
Proxmox firewall service is not running.
|
||||||
Run: systemctl enable --now pve-firewall
|
You can run: systemctl enable --now pve-firewall
|
||||||
when: pve_fw_status.rc != 0
|
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
|
# Corosync safety validation
|
||||||
@@ -89,180 +223,50 @@
|
|||||||
register: compiled_fw
|
register: compiled_fw
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: compiled_fw.rc != 0
|
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
|
- name: fail2ban | Fail if corosync ports are being dropped
|
||||||
ansible.builtin.fail:
|
ansible.builtin.debug:
|
||||||
msg: >
|
msg: >
|
||||||
Firewall configuration appears to affect Corosync ports (5404/5405).
|
Firewall configuration appears to affect Corosync ports (5404/5405).
|
||||||
Refusing to continue to prevent cluster outage.
|
Refusing to continue to prevent cluster outage.
|
||||||
when:
|
when:
|
||||||
- cluster_status.stat.exists
|
- clustered.stat.exists | default(false)
|
||||||
- compiled_fw.stdout is search('5404.*DROP|5405.*DROP')
|
- compiled_fw.stdout is search('5404.*DROP|5405.*DROP')
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
# Install Fail2Ban
|
# Deploy cluster-aware Fail2Ban action
|
||||||
#################################################
|
#################################################
|
||||||
|
|
||||||
- name: fail2ban | Install fail2ban
|
- name: fail2ban-fw | Deploy proxmox-fw action
|
||||||
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
|
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: /etc/fail2ban/action.d/proxmox-fw.conf
|
dest: /etc/fail2ban/action.d/proxmox-fw.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
content: |
|
content: |
|
||||||
[Definition]
|
[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 =
|
actionstart =
|
||||||
actionstop =
|
actionstop =
|
||||||
actioncheck =
|
when:
|
||||||
actionban = /usr/sbin/pve-firewall ipset add {{ f2b_ipset_name }} <ip>
|
- clustered.stat.exists | default(false)
|
||||||
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_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=<HOST>
|
|
||||||
pam_unix\(sshd:auth\): authentication failure;.*rhost=<HOST>
|
|
||||||
winbind.*authentication for user.*from <HOST> failed
|
|
||||||
ignoreregex =
|
|
||||||
notify:
|
notify:
|
||||||
- Restart fail2ban
|
- Restart fail2ban
|
||||||
|
|
||||||
@@ -299,7 +303,7 @@
|
|||||||
- name: fail2ban | Unban specific IP
|
- name: fail2ban | Unban specific IP
|
||||||
ansible.builtin.command: >
|
ansible.builtin.command: >
|
||||||
pve-firewall ipset del {{ f2b_ipset_name }} {{ f2b_unban_ip }}
|
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
|
register: unban_result
|
||||||
changed_when: "'removed' in unban_result.stdout or unban_result.rc == 0"
|
changed_when: "'removed' in unban_result.stdout or unban_result.rc == 0"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|||||||
Reference in New Issue
Block a user