mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-05 20:03:48 +00:00
466 lines
17 KiB
Bash
466 lines
17 KiB
Bash
#!/bin/bash
|
|
# ProxMenux - Fail2Ban Installer & Configurator
|
|
# ============================================
|
|
# Author : MacRimi
|
|
# License : MIT
|
|
# Version : 1.0
|
|
# ============================================
|
|
# 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 "$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() {
|
|
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"
|
|
cat > "$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 || \
|
|
! DEBIAN_FRONTEND=noninteractive apt-get install -y fail2ban >/dev/null 2>&1; 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
|
|
cat > /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
|
|
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...")"
|
|
cat > /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
|
|
systemctl enable --now proxmox-auth-logger.service >/dev/null 2>&1
|
|
msg_ok "$(translate "Proxmox auth logger service created and started")"
|
|
|
|
# -- SSH auth logger --
|
|
msg_info "$(translate "Creating SSH auth logger service...")"
|
|
cat > /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
|
|
systemctl enable --now ssh-auth-logger.service >/dev/null 2>&1
|
|
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...")"
|
|
cat > /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...")"
|
|
cat > /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...")"
|
|
cat > /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...")"
|
|
cat > /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...")"
|
|
cat > /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
|
|
echo "$original_max_auth" > "${BASE_DIR}/sshd_maxauthtries_backup"
|
|
|
|
msg_info "$(translate "Hardening SSH: setting MaxAuthTries to 3...")"
|
|
if grep -qi '^MaxAuthTries' "$sshd_config"; then
|
|
sed -i 's/^MaxAuthTries.*/MaxAuthTries 3/' "$sshd_config"
|
|
elif grep -qi '^#MaxAuthTries' "$sshd_config"; then
|
|
sed -i 's/^#MaxAuthTries.*/MaxAuthTries 3/' "$sshd_config"
|
|
else
|
|
echo "MaxAuthTries 3" >> "$sshd_config"
|
|
fi
|
|
|
|
# Reload SSH to apply the change (reload, not restart, to keep existing sessions)
|
|
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
|
|
systemctl enable fail2ban >/dev/null 2>&1
|
|
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() {
|
|
show_proxmenux_logo
|
|
msg_title "$(translate "$SCRIPT_TITLE")"
|
|
msg_info2 "$(translate "Removing Fail2Ban...")"
|
|
|
|
systemctl stop fail2ban 2>/dev/null || true
|
|
systemctl disable fail2ban 2>/dev/null || true
|
|
|
|
# Stop and remove the auth logger services
|
|
systemctl stop proxmox-auth-logger.service 2>/dev/null || true
|
|
systemctl disable proxmox-auth-logger.service 2>/dev/null || true
|
|
rm -f /etc/systemd/system/proxmox-auth-logger.service
|
|
systemctl stop ssh-auth-logger.service 2>/dev/null || true
|
|
systemctl disable ssh-auth-logger.service 2>/dev/null || true
|
|
rm -f /etc/systemd/system/ssh-auth-logger.service
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
rm -f /var/log/proxmox-auth.log /var/log/ssh-auth.log
|
|
|
|
DEBIAN_FRONTEND=noninteractive apt-get purge -y fail2ban >/dev/null 2>&1
|
|
rm -f /etc/fail2ban/jail.d/proxmox.conf
|
|
rm -f /etc/fail2ban/jail.d/proxmenux.conf
|
|
rm -f /etc/fail2ban/filter.d/proxmox.conf
|
|
rm -f /etc/fail2ban/filter.d/proxmenux.conf
|
|
rm -f /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
|
|
sed -i "s/^MaxAuthTries.*/MaxAuthTries ${original_val}/" "$sshd_config"
|
|
fi
|
|
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
|
|
msg_ok "$(translate "SSH MaxAuthTries restored to ${original_val}")"
|
|
fi
|
|
rm -f "$backup_file"
|
|
fi
|
|
|
|
# Remove journald drop-in and restore original log level
|
|
if [[ -f /etc/systemd/journald.conf.d/proxmenux-loglevel.conf ]]; then
|
|
rm -f /etc/systemd/journald.conf.d/proxmenux-loglevel.conf
|
|
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
|