diff --git a/scripts/samihost-fail2ban-watchdog.sh b/scripts/samihost-fail2ban-watchdog.sh new file mode 100755 index 0000000..b8b10c6 --- /dev/null +++ b/scripts/samihost-fail2ban-watchdog.sh @@ -0,0 +1,76 @@ +#!/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)"