All checks were successful
ansible-lint / Ansible Lint (push) Successful in 12s
Gitleaks Scan / gitleaks (push) Successful in 4s
Markdown Lint / markdown-lint (push) Successful in 8s
ai-reviews / Review PR (pull_request) Successful in 13s
PR check / Gitleaks (pull_request) Successful in 5s
PR check / lint tests (pull_request) Successful in 16s
PR check / labeler (pull_request) Successful in 2s
PR check / handle_failures (pull_request) Has been skipped
PR check / handle_success (pull_request) Successful in 1s
This commit refactors the task names for better readability and consistency. Additionally, it improves the formatting of the YAML file to enhance maintainability.
323 lines
9.7 KiB
YAML
323 lines
9.7 KiB
YAML
---
|
|
# -------------------------------------------------
|
|
# Deploy Fail2Ban integrated with Proxmox Firewall
|
|
# -------------------------------------------------
|
|
|
|
#################################################
|
|
# Detect Proxmox
|
|
#################################################
|
|
|
|
- name: fail2ban | Detect Proxmox
|
|
ansible.builtin.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
|
|
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
|
|
|
|
#################################################
|
|
# 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)
|
|
|
|
- name: fail2ban | Show firewall config path
|
|
ansible.builtin.debug:
|
|
msg: >
|
|
WARNING: Proxmox firewall config path is: {{ pve_firewall_config}}
|
|
when: pve_firewall_config is defined
|
|
|
|
#################################################
|
|
# 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 | Debug config contents
|
|
ansible.builtin.debug:
|
|
msg: >
|
|
{{ fw_content }}
|
|
when: not pve_firewall_enabled
|
|
|
|
- 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$', multiline=True)
|
|
}}
|
|
|
|
- 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 | 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
|