Files
ProxMenux/scripts/security/fail2ban_installer.sh
T
MacRimiandClaude Opus 5 da8a480eff Add audit and reports page, and a change journal
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer.

The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded.

The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:06:04 +02:00

509 lines
19 KiB
Bash

#!/bin/bash
# ==========================================================
# ProxMenux - Fail2Ban Installer & Configurator
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
# Version : 1.0
# ==========================================================
# Description:
# Installs and configures Fail2Ban to protect SSH, the Proxmox VE
# web UI (port 8006) and the ProxMenux Monitor (port 8008 + reverse
# proxy) against brute-force attacks. Hybrid runtime: works from
# terminal dialogs and from the ProxMenux web panel.
#
# Features:
# - Adjusts journald MaxLevelStore if Proxmox default ('warning')
# would silently drop SSH/PAM auth events.
# - Creates two journal-to-file logger services so Fail2Ban can use
# the reliable file backend instead of systemd journal (avoids
# known issues with pvedaemon worker / sshd journal reads).
# - Three jails: [sshd] (aggressive, 2 retries / 9h ban),
# [proxmox] (8006, 3 retries / 1h ban), [proxmenux] (8008 +
# http/https, 3 retries / 1h ban).
# - Auto-detects firewall backend (nftables preferred, iptables
# fallback) and sets the matching ban actions.
# - SSH hardening: sets MaxAuthTries=3 (Lynis SSH-7408 recommendation),
# backing up the original value for clean restore on uninstall.
# - Reinstall flow rewrites all jails with current defaults.
# - Clean uninstall: removes jails, logger services, journald drop-in
# and restores the original SSH MaxAuthTries.
# - Component status tracked in components_status.json.
# ==========================================================
# Hybrid script: works from terminal (dialog) and web panel (ScriptTerminalModal)
SCRIPT_TITLE="Fail2Ban Installer for Proxmox VE"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
COMPONENTS_STATUS_FILE="$BASE_DIR/components_status.json"
export BASE_DIR
export COMPONENTS_STATUS_FILE
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
fi
if [[ ! -f "$COMPONENTS_STATUS_FILE" ]]; then
echo "{}" > "$COMPONENTS_STATUS_FILE"
fi
load_language
initialize_cache
# ==========================================================
# Detection
# ==========================================================
detect_fail2ban() {
FAIL2BAN_INSTALLED=false
FAIL2BAN_VERSION=""
FAIL2BAN_ACTIVE=false
if command -v fail2ban-client >/dev/null 2>&1; then
FAIL2BAN_INSTALLED=true
FAIL2BAN_VERSION=$(fail2ban-client --version 2>/dev/null | head -n1 | tr -d '[:space:]' || echo "unknown")
if systemctl is-active --quiet fail2ban 2>/dev/null; then
FAIL2BAN_ACTIVE=true
fi
fi
}
# ==========================================================
# Installation
# ==========================================================
install_fail2ban() {
local FUNC_VERSION="1.0"
pmx_journal_context "install_fail2ban" "$FUNC_VERSION"
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
msg_info2 "$(translate "Installing and configuring Fail2Ban to protect Proxmox web interface and SSH...")"
# Ensure Debian repositories are available
local deb_codename
deb_codename=$(grep -oP '^VERSION_CODENAME=\K.*' /etc/os-release 2>/dev/null)
if ! grep -RqsE "debian.*(bookworm|trixie)" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null; then
msg_warn "$(translate "Debian repositories missing; creating default source file")"
local src="/etc/apt/sources.list.d/debian.sources"
pmx_write_file "$src" <<EOF
Types: deb
URIs: http://deb.debian.org/debian
Suites: ${deb_codename} ${deb_codename}-updates
Components: main contrib non-free non-free-firmware
Types: deb
URIs: http://security.debian.org/debian-security
Suites: ${deb_codename}-security
Components: main contrib non-free non-free-firmware
EOF
msg_ok "$(translate "Debian repositories configured for ${deb_codename}")"
fi
# Install Fail2Ban
msg_info "$(translate "Installing Fail2Ban...")"
if ! DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1 || \
! pmx_install_pkg fail2ban; then
msg_error "$(translate "Failed to install Fail2Ban")"
return 1
fi
msg_ok "$(translate "Fail2Ban installed successfully")"
# ── Ensure journald stores auth-level messages ──
# Proxmox sets MaxLevelStore=warning by default, which silently drops
# info/notice messages. SSH auth failures (PAM) are logged at info/notice,
# so Fail2Ban with backend=systemd will never see them.
local journald_conf="/etc/systemd/journald.conf"
local journald_changed=false
if [[ -f "$journald_conf" ]]; then
local current_max
current_max=$(grep -i '^MaxLevelStore=' "$journald_conf" 2>/dev/null | tail -1 | cut -d= -f2 | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
# Levels that are too restrictive for auth logging (need at least info)
case "$current_max" in
emerg|alert|crit|err|warning)
msg_warn "$(translate "journald MaxLevelStore is '${current_max}' - SSH auth failures are not being stored")"
msg_info "$(translate "Updating journald to store info-level messages...")"
# Create a drop-in so we don't break other Proxmox settings
mkdir -p /etc/systemd/journald.conf.d
pmx_write_file /etc/systemd/journald.conf.d/proxmenux-loglevel.conf <<'JEOF'
# ProxMenux: Allow auth/info messages so Fail2Ban can detect SSH failures
# Proxmox default MaxLevelStore=warning drops PAM/SSH auth events
[Journal]
MaxLevelStore=info
MaxLevelSyslog=info
JEOF
journald_changed=true
msg_ok "$(translate "journald drop-in created: /etc/systemd/journald.conf.d/proxmenux-loglevel.conf")"
;;
*)
msg_ok "$(translate "journald MaxLevelStore is adequate for auth logging")"
;;
esac
if $journald_changed; then
pmx_record_execution "restart systemd-journald" "systemctl restart systemd-journald"
systemctl restart systemd-journald
sleep 1
msg_ok "$(translate "journald restarted - auth messages will now be stored")"
fi
fi
# ── Journal-to-file logger services for Fail2Ban ──
# Fail2Ban's systemd backend has a known issue: it cannot reliably read
# journal entries in real-time from certain services (pvedaemon workers,
# and intermittently sshd). The solution is to create small systemd services
# that tail the journal and write to log files, then fail2ban monitors those
# files with the reliable backend=auto.
# -- Proxmox UI auth logger (pvedaemon) --
msg_info "$(translate "Creating Proxmox auth logger service...")"
pmx_write_file /etc/systemd/system/proxmox-auth-logger.service <<'EOF'
[Unit]
Description=Proxmox Auth Logger for Fail2Ban
Documentation=https://github.com/MacRimi/ProxMenux
After=pvedaemon.service
PartOf=fail2ban.service
[Service]
Type=simple
ExecStart=/bin/bash -c 'journalctl -f _SYSTEMD_UNIT=pvedaemon.service -o short-iso --no-pager >> /var/log/proxmox-auth.log'
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
touch /var/log/proxmox-auth.log
chmod 640 /var/log/proxmox-auth.log
chown root:adm /var/log/proxmox-auth.log 2>/dev/null || true
systemctl daemon-reload
pmx_enable_service proxmox-auth-logger.service
msg_ok "$(translate "Proxmox auth logger service created and started")"
# -- SSH auth logger --
msg_info "$(translate "Creating SSH auth logger service...")"
pmx_write_file /etc/systemd/system/ssh-auth-logger.service <<'EOF'
[Unit]
Description=SSH Auth Logger for Fail2Ban
Documentation=https://github.com/MacRimi/ProxMenux
After=ssh.service
PartOf=fail2ban.service
[Service]
Type=simple
ExecStart=/bin/bash -c 'journalctl -f _SYSTEMD_UNIT=ssh.service -o short-iso --no-pager >> /var/log/ssh-auth.log'
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
touch /var/log/ssh-auth.log
chmod 640 /var/log/ssh-auth.log
chown root:adm /var/log/ssh-auth.log 2>/dev/null || true
systemctl daemon-reload
pmx_enable_service ssh-auth-logger.service
msg_ok "$(translate "SSH auth logger service created and started")"
# Configure Proxmox filter
mkdir -p /etc/fail2ban/filter.d /etc/fail2ban/jail.d
msg_info "$(translate "Configuring Proxmox filter...")"
pmx_write_file /etc/fail2ban/filter.d/proxmox.conf <<'EOF'
[Definition]
# The proxmox-auth-logger service writes journal lines to /var/log/proxmox-auth.log
# in short-iso format: 2026-02-10T19:36:08+01:00 host pvedaemon[PID]: message
# Proxmox logs IPs as ::ffff:x.x.x.x (IPv4-mapped IPv6).
failregex = authentication (failure|error); rhost=(::ffff:)?<HOST> user=.* msg=.*
ignoreregex =
datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S
EOF
msg_ok "$(translate "Proxmox filter configured")"
# Configure Proxmox jail (file-based backend)
msg_info "$(translate "Configuring Proxmox jail...")"
pmx_write_file /etc/fail2ban/jail.d/proxmox.conf <<'EOF'
[proxmox]
enabled = true
port = 8006
filter = proxmox
backend = auto
logpath = /var/log/proxmox-auth.log
maxretry = 3
bantime = 3600
findtime = 600
EOF
msg_ok "$(translate "Proxmox jail configured")"
# Configure ProxMenux Monitor filter
# This reads from a file written directly by the Flask app (not syslog/journal),
# so it uses a datepattern that matches Python's logging format.
msg_info "$(translate "Configuring ProxMenux Monitor filter...")"
pmx_write_file /etc/fail2ban/filter.d/proxmenux.conf <<'EOF'
[Definition]
failregex = ^.*proxmenux-auth: authentication failure; rhost=<HOST> user=.*$
ignoreregex =
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
EOF
msg_ok "$(translate "ProxMenux Monitor filter configured")"
# Configure ProxMenux Monitor jail (port 8008 + http/https for reverse proxy)
# Uses backend=auto with logpath because the Flask app writes directly to this file.
msg_info "$(translate "Configuring ProxMenux Monitor jail...")"
pmx_write_file /etc/fail2ban/jail.d/proxmenux.conf <<'EOF'
[proxmenux]
enabled = true
port = 8008,http,https
filter = proxmenux
backend = auto
logpath = /var/log/proxmenux-auth.log
maxretry = 3
bantime = 3600
findtime = 600
EOF
msg_ok "$(translate "ProxMenux Monitor jail configured")"
# Ensure ProxMenux auth log exists (Flask writes here directly)
touch /var/log/proxmenux-auth.log
chmod 640 /var/log/proxmenux-auth.log 2>/dev/null || true
# Detect firewall backend (nftables preferred, fallback to iptables)
local ban_action="iptables-multiport"
local ban_action_all="iptables-allports"
if command -v nft >/dev/null 2>&1 && nft list ruleset >/dev/null 2>&1; then
ban_action="nftables"
ban_action_all="nftables[type=allports]"
msg_ok "$(translate "Detected nftables - using nftables ban action")"
else
msg_info "$(translate "nftables not available - using iptables ban action")"
fi
# Configure global settings and SSH jail
msg_info "$(translate "Configuring global Fail2Ban settings and SSH jail...")"
pmx_write_file /etc/fail2ban/jail.local <<EOF
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
ignoreself = true
bantime = 86400
maxretry = 2
findtime = 1800
backend = auto
banaction = ${ban_action}
banaction_allports = ${ban_action_all}
[sshd]
enabled = true
filter = sshd[mode=aggressive]
backend = auto
logpath = /var/log/ssh-auth.log
maxretry = 2
findtime = 3600
bantime = 32400
EOF
msg_ok "$(translate "Global settings and SSH jail configured")"
# ── SSH Hardening: MaxAuthTries ──
# Lynis (SSH-7408) recommends MaxAuthTries=3. With fail2ban maxretry=2,
# SSH will never reach 3 attempts, but setting it satisfies the audit
# and adds defense-in-depth. Backup original value for clean restore.
local sshd_config="/etc/ssh/sshd_config"
if [[ -f "$sshd_config" ]]; then
# Save original MaxAuthTries value for restore on uninstall
local original_max_auth
original_max_auth=$(grep -i '^MaxAuthTries' "$sshd_config" 2>/dev/null | awk '{print $2}' || echo "6")
if [[ -z "$original_max_auth" ]]; then
original_max_auth="6"
fi
# Store original value in our config directory
printf '%s\n' "$original_max_auth" | pmx_write_file "${BASE_DIR}/sshd_maxauthtries_backup"
msg_info "$(translate "Hardening SSH: setting MaxAuthTries to 3...")"
if grep -qi '^MaxAuthTries' "$sshd_config"; then
pmx_edit_file "$sshd_config" 's/^MaxAuthTries.*/MaxAuthTries 3/'
elif grep -qi '^#MaxAuthTries' "$sshd_config"; then
pmx_edit_file "$sshd_config" 's/^#MaxAuthTries.*/MaxAuthTries 3/'
else
echo "MaxAuthTries 3" | pmx_append_file "$sshd_config"
fi
# Reload SSH to apply the change (reload, not restart, to keep existing sessions)
pmx_record_execution "reload SSH service" "systemctl reload sshd or ssh"
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
msg_ok "$(translate "SSH MaxAuthTries set to 3 (original: ${original_max_auth})")"
fi
# Enable and restart the service (restart ensures new jails are loaded
# even if fail2ban was already running from a previous install)
systemctl daemon-reload
pmx_apply_setting "fail2ban enabled state" "systemctl is-enabled fail2ban 2>/dev/null || true" \
systemctl enable fail2ban
pmx_record_execution "restart fail2ban" "systemctl restart fail2ban"
systemctl restart fail2ban >/dev/null 2>&1
sleep 3
# Verify
if systemctl is-active --quiet fail2ban; then
msg_ok "$(translate "Fail2Ban is running correctly")"
else
msg_error "$(translate "Fail2Ban is NOT running!")"
journalctl -u fail2ban --no-pager -n 10
fi
if fail2ban-client ping >/dev/null 2>&1; then
msg_ok "$(translate "fail2ban-client successfully communicated with the server")"
else
msg_error "$(translate "fail2ban-client could not communicate with the server")"
fi
update_component_status "fail2ban" "installed" "$(fail2ban-client --version 2>/dev/null | head -n1)" "security" '{}'
msg_success "$(translate "Fail2Ban installation and configuration completed successfully!")"
}
# ==========================================================
# Uninstall
# ==========================================================
uninstall_fail2ban() {
local FUNC_VERSION="1.0"
pmx_journal_context "uninstall_fail2ban" "$FUNC_VERSION"
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
msg_info2 "$(translate "Removing Fail2Ban...")"
pmx_disable_service fail2ban 2>/dev/null || true
# Stop and remove the auth logger services
pmx_disable_service proxmox-auth-logger.service 2>/dev/null || true
pmx_remove_file /etc/systemd/system/proxmox-auth-logger.service
pmx_disable_service ssh-auth-logger.service 2>/dev/null || true
pmx_remove_file /etc/systemd/system/ssh-auth-logger.service
systemctl daemon-reload 2>/dev/null || true
pmx_record_execution "remove Fail2Ban auth logger files" \
"rm -f /var/log/proxmox-auth.log /var/log/ssh-auth.log"
rm -f /var/log/proxmox-auth.log /var/log/ssh-auth.log
pmx_record_execution "purge fail2ban package" "apt-get purge -y fail2ban"
DEBIAN_FRONTEND=noninteractive apt-get purge -y fail2ban >/dev/null 2>&1
pmx_remove_file /etc/fail2ban/jail.d/proxmox.conf
pmx_remove_file /etc/fail2ban/jail.d/proxmenux.conf
pmx_remove_file /etc/fail2ban/filter.d/proxmox.conf
pmx_remove_file /etc/fail2ban/filter.d/proxmenux.conf
pmx_remove_file /etc/fail2ban/jail.local
# ── Restore SSH MaxAuthTries to original value ──
local sshd_config="/etc/ssh/sshd_config"
local backup_file="${BASE_DIR}/sshd_maxauthtries_backup"
if [[ -f "$backup_file" && -f "$sshd_config" ]]; then
local original_val
original_val=$(cat "$backup_file" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$original_val" ]]; then
msg_info "$(translate "Restoring SSH MaxAuthTries to ${original_val}...")"
if grep -qi '^MaxAuthTries' "$sshd_config"; then
pmx_edit_file "$sshd_config" "s/^MaxAuthTries.*/MaxAuthTries ${original_val}/"
fi
pmx_record_execution "reload SSH service" "systemctl reload sshd or ssh"
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
msg_ok "$(translate "SSH MaxAuthTries restored to ${original_val}")"
fi
pmx_remove_file "$backup_file"
fi
# Remove journald drop-in and restore original log level
if [[ -f /etc/systemd/journald.conf.d/proxmenux-loglevel.conf ]]; then
pmx_remove_file /etc/systemd/journald.conf.d/proxmenux-loglevel.conf
pmx_record_execution "restart systemd-journald" "systemctl restart systemd-journald"
systemctl restart systemd-journald 2>/dev/null || true
msg_ok "$(translate "journald log level restored")"
fi
update_component_status "fail2ban" "removed" "" "security" '{}'
msg_ok "$(translate "Fail2Ban has been removed")"
msg_success "$(translate "Uninstallation completed. Press Enter to continue...")"
read -r
}
# ==========================================================
# Main
# ==========================================================
main() {
detect_fail2ban
if $FAIL2BAN_INSTALLED; then
# Already installed - show action menu
local action_text
action_text="\n$(translate 'Fail2Ban is currently installed.')\n"
action_text+="$(translate 'Version:') $FAIL2BAN_VERSION\n"
action_text+="$(translate 'Status:') $(if $FAIL2BAN_ACTIVE; then translate 'Active'; else translate 'Inactive'; fi)\n\n"
action_text+="$(translate 'What would you like to do?')"
local ACTION
ACTION=$(hybrid_menu "$(translate 'Fail2Ban Management')" "$action_text" 18 70 3 \
"reinstall" "$(translate 'Reinstall and reconfigure Fail2Ban')" \
"remove" "$(translate 'Uninstall Fail2Ban')" \
"cancel" "$(translate 'Cancel')" \
) || ACTION="cancel"
case "$ACTION" in
reinstall)
if hybrid_yesno "$(translate 'Reinstall Fail2Ban')" \
"\n\n$(translate 'This will reinstall and reconfigure Fail2Ban with the default ProxMenux settings. Continue?')" 12 70; then
install_fail2ban
fi
;;
remove)
if hybrid_yesno "$(translate 'Remove Fail2Ban')" \
"\n\n$(translate 'This will completely remove Fail2Ban and its configuration. Continue?')" 12 70; then
uninstall_fail2ban
fi
;;
cancel|*)
exit 0
;;
esac
else
# Not installed - confirm and install
local info_text
info_text="\n$(translate 'Fail2Ban is not installed on this system.')\n\n"
info_text+="$(translate 'This will install and configure Fail2Ban with:')\n\n"
info_text+=" - $(translate 'SSH protection (aggressive mode)') (max 2 $(translate 'retries'), 9h $(translate 'ban'))\n"
info_text+=" - $(translate 'Proxmox web interface protection') ($(translate 'port') 8006, max 3 $(translate 'retries'), 1h $(translate 'ban'))\n"
info_text+=" - $(translate 'ProxMenux Monitor protection') ($(translate 'port') 8008 + $(translate 'reverse proxy'), max 3 $(translate 'retries'), 1h $(translate 'ban'))\n"
info_text+=" - $(translate 'Auto-detected firewall backend (nftables/iptables)')\n"
info_text+=" - $(translate 'Adjusts journald log level if needed (Proxmox defaults may block auth logs)')\n"
info_text+=" - $(translate 'SSH hardening: MaxAuthTries set to 3 (Lynis recommendation)')\n\n"
info_text+="$(translate 'Do you want to proceed?')"
if hybrid_yesno "$(translate 'Install Fail2Ban')" "$info_text" 20 70; then
install_fail2ban
else
exit 0
fi
fi
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main
fi