#!/bin/bash ################################################################################ # Script Name: install-fail2ban.sh # Version: 1.0 # Description: Automated Fail2ban installation with multi-jail configuration, # custom ban actions, allowlists, Prometheus metrics integration, # and service auto-detection for Debian/Ubuntu and RHEL/Rocky/Alma # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Usage: # sudo ./install-fail2ban.sh # sudo ./install-fail2ban.sh --jails "sshd,nginx-http-auth,postfix" # sudo ./install-fail2ban.sh --allowlist "10.0.0.0/8,192.168.1.0/24" # sudo ./install-fail2ban.sh --ban-action nftables --bantime 3600 # sudo ./install-fail2ban.sh --dry-run # sudo ./install-fail2ban.sh --uninstall # ################################################################################ set -euo pipefail # ============================================================================ # DEFAULTS # ============================================================================ readonly VERSION="1.0" readonly SCRIPT_NAME="${0##*/}" readonly LOG_FILE="/var/log/fail2ban-install.log" JAILS="" ALLOWLIST="127.0.0.1/8 ::1" BAN_ACTION="auto" BANTIME="3600" FINDTIME="600" MAXRETRY="5" BACKEND="auto" DRY_RUN=false UNINSTALL=false PROMETHEUS=false RECIDIVE=true # OS detection OS_ID="" OS_VERSION="" PKG_MGR="" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # ============================================================================ # HELPER FUNCTIONS # ============================================================================ log_info() { echo -e "${GREEN}[INFO]${NC} $*"; echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE" 2>/dev/null || true; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE" 2>/dev/null || true; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE" 2>/dev/null || true; } log_step() { echo -e "${CYAN}[STEP]${NC} $*"; echo "[STEP] $(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE" 2>/dev/null || true; } show_usage() { cat </dev/null; then PKG_MGR="yum" fi ;; *) log_error "Unsupported OS: $OS_ID" exit 1 ;; esac log_info "Detected OS: $OS_ID $OS_VERSION (package manager: $PKG_MGR)" } # ============================================================================ # SERVICE DETECTION # ============================================================================ detect_services() { if [ -n "$JAILS" ]; then log_info "Using specified jails: $JAILS" return fi log_step "Auto-detecting services for jail configuration..." local detected=() # SSH is always enabled detected+=("sshd") # Nginx if systemctl is-active --quiet nginx 2>/dev/null || [ -f /etc/nginx/nginx.conf ]; then detected+=("nginx-http-auth") detected+=("nginx-botsearch") log_info " Detected: nginx" fi # Apache if systemctl is-active --quiet httpd 2>/dev/null || systemctl is-active --quiet apache2 2>/dev/null; then detected+=("apache-auth") detected+=("apache-badbots") log_info " Detected: apache" fi # Postfix if systemctl is-active --quiet postfix 2>/dev/null; then detected+=("postfix") log_info " Detected: postfix" fi # Dovecot if systemctl is-active --quiet dovecot 2>/dev/null; then detected+=("dovecot") log_info " Detected: dovecot" fi # MySQL/MariaDB if systemctl is-active --quiet mysqld 2>/dev/null || systemctl is-active --quiet mariadb 2>/dev/null; then detected+=("mysqld-auth") log_info " Detected: mysql/mariadb" fi # BIND if systemctl is-active --quiet named 2>/dev/null || systemctl is-active --quiet bind9 2>/dev/null; then detected+=("named-refused") log_info " Detected: bind/named" fi JAILS=$(IFS=','; echo "${detected[*]}") log_info "Auto-detected jails: $JAILS" } # ============================================================================ # BAN ACTION DETECTION # ============================================================================ detect_ban_action() { if [ "$BAN_ACTION" != "auto" ]; then log_info "Using specified ban action: $BAN_ACTION" return fi if command -v nft &>/dev/null && nft list ruleset &>/dev/null; then BAN_ACTION="nftables-multiport" log_info "Detected nftables — using nftables-multiport action" elif command -v iptables &>/dev/null; then BAN_ACTION="iptables-multiport" log_info "Detected iptables — using iptables-multiport action" else BAN_ACTION="iptables-multiport" log_warn "No firewall detected — defaulting to iptables-multiport" fi } # ============================================================================ # INSTALLATION # ============================================================================ install_fail2ban() { log_step "Installing Fail2ban..." if $DRY_RUN; then log_info "[DRY RUN] Would install fail2ban via $PKG_MGR" return fi case "$PKG_MGR" in apt) apt-get update -qq apt-get install -y -qq fail2ban ;; dnf|yum) $PKG_MGR install -y -q epel-release 2>/dev/null || true $PKG_MGR install -y -q fail2ban fail2ban-firewalld 2>/dev/null || \ $PKG_MGR install -y -q fail2ban ;; esac log_info "Fail2ban installed successfully" } # ============================================================================ # CONFIGURATION # ============================================================================ configure_fail2ban() { log_step "Configuring Fail2ban..." local jail_local="/etc/fail2ban/jail.local" if $DRY_RUN; then log_info "[DRY RUN] Would write configuration to $jail_local" show_config return fi # Backup existing config if [ -f "$jail_local" ]; then cp "$jail_local" "${jail_local}.bak.$(date +%s)" log_info "Backed up existing $jail_local" fi # Write jail.local cat > "$jail_local" <> "$jail_local" done # Recidive jail if $RECIDIVE; then cat >> "$jail_local" <<'RECIDIVEEOF' [recidive] enabled = true logpath = /var/log/fail2ban.log bantime = 604800 findtime = 86400 maxretry = 3 RECIDIVEEOF log_info "Recidive jail enabled (repeat offenders banned for 7 days)" fi log_info "Configuration written to $jail_local" } write_jail_config() { local jail="$1" case "$jail" in sshd) cat <<'EOF' [sshd] enabled = true port = ssh logpath = %(sshd_log)s maxretry = 5 EOF ;; nginx-http-auth) cat <<'EOF' [nginx-http-auth] enabled = true port = http,https logpath = /var/log/nginx/error.log maxretry = 5 EOF ;; nginx-botsearch) cat <<'EOF' [nginx-botsearch] enabled = true port = http,https logpath = /var/log/nginx/access.log maxretry = 2 EOF ;; nginx-limit-req) cat <<'EOF' [nginx-limit-req] enabled = true port = http,https logpath = /var/log/nginx/error.log maxretry = 5 EOF ;; apache-auth) cat <<'EOF' [apache-auth] enabled = true port = http,https logpath = %(apache_error_log)s maxretry = 5 EOF ;; apache-badbots) cat <<'EOF' [apache-badbots] enabled = true port = http,https logpath = %(apache_access_log)s maxretry = 2 bantime = 86400 EOF ;; postfix) cat <<'EOF' [postfix] enabled = true port = smtp,465,submission logpath = /var/log/mail.log maxretry = 5 EOF ;; dovecot) cat <<'EOF' [dovecot] enabled = true port = pop3,pop3s,imap,imaps logpath = /var/log/mail.log maxretry = 5 EOF ;; mysqld-auth) cat <<'EOF' [mysqld-auth] enabled = true port = 3306 logpath = /var/log/mysql/error.log maxretry = 5 EOF ;; named-refused) cat <<'EOF' [named-refused] enabled = true port = domain,953 logpath = /var/log/named/security.log maxretry = 5 EOF ;; *) log_warn "Unknown jail: $jail — skipping" ;; esac } show_config() { echo "" echo "=== Generated Configuration ===" echo "[DEFAULT]" echo "bantime = $BANTIME" echo "findtime = $FINDTIME" echo "maxretry = $MAXRETRY" echo "banaction = $BAN_ACTION" echo "ignoreip = $ALLOWLIST" echo "" IFS=',' read -ra JAIL_ARRAY <<< "$JAILS" for jail in "${JAIL_ARRAY[@]}"; do jail=$(echo "$jail" | xargs) echo "[$jail] = enabled" done if $RECIDIVE; then echo "[recidive] = enabled (7-day ban for repeat offenders)" fi echo "================================" } # ============================================================================ # PROMETHEUS INTEGRATION # ============================================================================ setup_prometheus() { if ! $PROMETHEUS; then return fi log_step "Setting up Prometheus metrics integration..." if $DRY_RUN; then log_info "[DRY RUN] Would download fail2ban-exporter.sh" return fi if [ -f /usr/local/bin/fail2ban-exporter.sh ]; then log_info "fail2ban-exporter.sh already exists" return fi local exporter_url="https://mylinux.work/downloads/fail2ban-exporter.sh" if curl -fsSL "$exporter_url" -o /usr/local/bin/fail2ban-exporter.sh 2>/dev/null; then chmod +x /usr/local/bin/fail2ban-exporter.sh log_info "Downloaded fail2ban-exporter.sh" # Add cron job if ! crontab -l 2>/dev/null | grep -q "fail2ban-exporter"; then (crontab -l 2>/dev/null; echo "*/2 * * * * /usr/local/bin/fail2ban-exporter.sh --textfile 2>/dev/null") | crontab - log_info "Added cron job for fail2ban-exporter" fi else log_warn "Could not download fail2ban-exporter.sh — configure manually" fi } # ============================================================================ # SERVICE MANAGEMENT # ============================================================================ start_fail2ban() { log_step "Starting Fail2ban service..." if $DRY_RUN; then log_info "[DRY RUN] Would enable and start fail2ban" return fi systemctl enable fail2ban systemctl restart fail2ban sleep 2 if systemctl is-active --quiet fail2ban; then log_info "Fail2ban is running" else log_error "Fail2ban failed to start — check journalctl -u fail2ban" exit 1 fi } # ============================================================================ # VERIFICATION # ============================================================================ verify_installation() { log_step "Verifying installation..." if $DRY_RUN; then log_info "[DRY RUN] Would verify installation" return fi echo "" echo "=== Fail2ban Status ===" fail2ban-client status echo "" IFS=',' read -ra JAIL_ARRAY <<< "$JAILS" for jail in "${JAIL_ARRAY[@]}"; do jail=$(echo "$jail" | xargs) echo "--- $jail ---" fail2ban-client status "$jail" 2>/dev/null || echo " (jail not active — check logs)" echo "" done if $RECIDIVE; then echo "--- recidive ---" fail2ban-client status recidive 2>/dev/null || echo " (recidive not active)" echo "" fi echo "=== Configuration ===" echo "Ban action: $BAN_ACTION" echo "Ban time: ${BANTIME}s ($(( BANTIME / 60 )) minutes)" echo "Find time: ${FINDTIME}s ($(( FINDTIME / 60 )) minutes)" echo "Max retry: $MAXRETRY" echo "Ignore list: $ALLOWLIST" echo "" log_info "Installation complete. Logs: $LOG_FILE" } # ============================================================================ # UNINSTALL # ============================================================================ uninstall_fail2ban() { log_step "Uninstalling Fail2ban..." if $DRY_RUN; then log_info "[DRY RUN] Would stop and remove fail2ban" return fi systemctl stop fail2ban 2>/dev/null || true systemctl disable fail2ban 2>/dev/null || true case "$PKG_MGR" in apt) apt-get purge -y -qq fail2ban apt-get autoremove -y -qq ;; dnf|yum) $PKG_MGR remove -y -q fail2ban ;; esac # Clean up config if [ -d /etc/fail2ban ]; then log_info "Configuration directory /etc/fail2ban preserved — remove manually if desired" fi log_info "Fail2ban uninstalled" exit 0 } # ============================================================================ # MAIN # ============================================================================ main() { parse_args "$@" echo "" echo "============================================" echo " Fail2ban Install Script v${VERSION}" echo " https://mylinux.work" echo "============================================" echo "" check_root detect_os if $UNINSTALL; then uninstall_fail2ban fi detect_services detect_ban_action install_fail2ban configure_fail2ban setup_prometheus start_fail2ban verify_installation } main "$@"