#!/bin/bash # Samihost fail2ban watchdog — auto-unban whitelisted IPs and keep ignoreip list in sync. # Deployed to /usr/local/bin/samihost-fail2ban-watchdog.sh on 194.163.161.162 # Cron: every 30 min (0,30 * * * *) set -euo pipefail JAIL_LOCAL=/etc/fail2ban/jail.local BACKUP=/etc/fail2ban/jail.local.watchdog.bak EXPECTED_IGNOREIP="127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 fc00::/7 fe80::/10 100.64.0.0/10 100.121.150.22 100.85.236.10 100.71.97.12 100.81.59.99 100.98.123.59 194.233.88.206 173.212.201.200 194.163.161.162" LOG=/var/log/samihost-fail2ban-watchdog.log TELEGRAM_LOG=/tmp/fail2ban-watchdog-last-action ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } log() { echo "$(ts) $*" | tee -a "$LOG"; } mkdir -p "$(dirname "$LOG")" touch "$LOG" # --- 1. Verify ignoreip line is intact and matches expected --- CURRENT=$(grep '^ignoreip' "$JAIL_LOCAL" | sed 's/^ignoreip[[:space:]]*=[[:space:]]*//' || true) EXPECTED_NORMALIZED=$(echo "$EXPECTED_IGNOREIP" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//') CURRENT_NORMALIZED=$(echo "$CURRENT" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//') if [ "$CURRENT_NORMALIZED" != "$EXPECTED_NORMALIZED" ]; then log "ALERT: ignoreip line drifted. Restoring." cp "$JAIL_LOCAL" "$BACKUP" sed -i "s|^ignoreip = .*|ignoreip = $EXPECTED_IGNOREIP|" "$JAIL_LOCAL" fail2ban-client reload echo "ignoreip restored at $(ts)" > "$TELEGRAM_LOG" log "ignoreip restored, fail2ban reloaded" fi # --- 2. Unban any currently-banned IPs that match our trusted set --- BANNED=$(fail2ban-client status sshd 2>/dev/null | awk -F: '/Banned IP list/{print $2}' | tr ' ' '\n' | grep -v '^$' || true) UNBANNED=0 for ip in $BANNED; do # Match against any trusted network is_trusted=0 for net in 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 ::1 fc00::/7 fe80::/10 100.121.150.22 100.85.236.10 100.71.97.12 100.81.59.99 100.98.123.59 194.233.88.206 173.212.201.200 194.163.161.162; do if [[ "$net" == *"/"* ]]; then # CIDR match (simple IPv4 only — IPv6 needs python or ipcalc, skip for now) base="${net%/*}" mask="${net#*/}" if [[ "$ip" == "$base"* ]] || python3 -c "import ipaddress,sys; sys.exit(0 if ipaddress.ip_address('$ip') in ipaddress.ip_network('$net', strict=False) else 1)" 2>/dev/null; then is_trusted=1 break fi else if [ "$ip" = "$net" ]; then is_trusted=1 break fi fi done if [ "$is_trusted" = "1" ]; then if fail2ban-client set sshd unbanip "$ip" >/dev/null 2>&1; then log "auto-unbanned trusted IP: $ip" UNBANNED=$((UNBANNED+1)) fi fi done [ "$UNBANNED" -gt 0 ] && echo "auto-unbanned $UNBANNED trusted IPs at $(ts)" > "$TELEGRAM_LOG" # --- 3. Cap the ban count — if more than 200 are banned, mass-unban stale ones --- TOTAL_BANNED=$(fail2ban-client status sshd 2>/dev/null | awk '/Currently banned/{print $NF}' || echo 0) if [ "$TOTAL_BANNED" -gt 200 ]; then log "ALERT: $TOTAL_BANNED IPs banned. Mass-unbanning all." for ip in $BANNED; do fail2ban-client set sshd unbanip "$ip" >/dev/null 2>&1 || true done echo "mass-unbanned $TOTAL_BANNED stale bans at $(ts)" > "$TELEGRAM_LOG" fi log "watchdog run complete (unbanned=$UNBANNED, total_banned=$TOTAL_BANNED)"