Skip to content

11 Fail2Ban Verification and Rollback

Mikhail Deynekin edited this page Oct 20, 2026 · 1 revision

Fail2Ban: Verification and Rollback

"It is configured" is not evidence. This page is the acceptance checklist: prove the filter matches, the jail bans, the action fires, the queue receives the event and AbuseIPDB accepts the report — then know how to undo everything.


Verification matrix

# What to prove Command Expected
1 configuration parses fail2ban-client -t OK: configuration test is successful
2 jail is enabled fail2ban-client status jail name in Jail list
3 action is attached fail2ban-client get <jail> actions nftables, abuseipdb-async
4 filter matches real abuse fail2ban-regex <log> <filter> matches > 0, only attack lines
5 filter ignores normal traffic fail2ban-regex <normal-sample> <filter> 0 matched
6 a ban is actually applied fail2ban-client set <jail> banip <doc IP> firewall set contains the IP
7 the queue receives bans abuseipdb-report --stats pending events grows
8 the payload is correct abuseipdb-report --dry-run JSON with comment/categories
9 the API accepts it abuseipdb-report -> 200 in the log
10 the report is visible check API with -d verbose your comment in reports[]
11 the schedule works systemctl list-timers abuseipdb-report.timer next run in ≤16 min
12 logs rotate logrotate -d /etc/logrotate.d/abuseipdb-reporter no error

Step 1–2: configuration and state

fail2ban-client -t
fail2ban-client status
fail2ban-client status sshd

Currently failed / Total failed count filter matches, not bans. A rising Currently failed with Currently banned: 0 means the threshold is not reached yet or ignoreip swallowed the address.

Step 3: actions attached

fail2ban-client get sshd actions
# The jail sshd has the following actions: nftables, abuseipdb-async

If abuseipdb-async is missing, the installer was not applied to that jail or fail2ban was not restarted after adding the action. Re-run:

cd /path/to/Fail2Ban_AbuseIPDB
sudo ./install.sh --jails "sshd <other jails>"

Step 4–5: filter tests

# positive
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf | tail -5
# negative: normal pages must not match
grep -h '"GET / ' /var/www/httpd-logs/*.access.log | head -200 > /tmp/normal.log
fail2ban-regex /tmp/normal.log /etc/fail2ban/filter.d/nginx-attack-scan.conf

A positive test with zero matches means: wrong path, wrong format, date-stripping assumption or a typo. See Fundamentals.

Step 6: ban a documentation address

⚠️ Never use your own IP. fail2ban-client set ... banip bypasses ignoreip.

fail2ban-client set sshd banip 198.51.100.77
nft list set inet f2b-table addr-set-sshd
fail2ban-client status sshd
fail2ban-client set sshd unbanip 198.51.100.77

For a web jail the set is addr-set-nginx-attack-scan; for the recidive jail it is addr-set-recidive.

If the address appears in fail2ban-client status but not in nftables, the ban action is broken (missing nftables, wrong chain, unsupported kernel). Check:

nft list table inet f2b-table
journalctl -u fail2ban --since "-10 min"

Step 7–9: queue, payload, report

/usr/local/sbin/abuseipdb-report --stats
/usr/local/sbin/abuseipdb-report --dry-run
/usr/local/sbin/abuseipdb-report
tail -n 20 /var/log/abuseipdb-reporter.log

Expected:

report: run start, pending_ips=1 budget=25 today=0
reported ip=198.51.100.77 jail=sshd cats=18,22 size=402 -> 200 {"data":{"abuseConfidenceScore":100}}
report: run done sent=1 skipped=0 errors=0 pending=0

Step 10: confirm on AbuseIPDB

KEY_FILE=/etc/abuseipdb/.abuseipdb.key
curl -s -G "https://api.abuseipdb.com/api/v2/check" \
  --data-urlencode "ipAddress=198.51.100.77" -d maxAgeInDays=1 -d verbose \
  -H "Key: $(cat "$KEY_FILE")" -H "Accept: application/json" \
  | python3 -c 'import json,sys; d=json.load(sys.stdin)["data"]; print(d["abuseConfidenceScore"]); [print(r["reportedAt"], r["comment"][:200]) for r in d.get("reports", [])]'

Our report may not be the newest one (the timestamp is the ban time, and other reporters may have newer entries). Search the returned reports[] for your server marker or attribution footer.

Step 11–12: schedule and logs

systemctl list-timers abuseipdb-report.timer
cat /etc/cron.d/abuseipdb-reporter          # when --cron was used
logrotate -d /etc/logrotate.d/abuseipdb-reporter

Audit: what actually attacks you

# top SSH sources (auth.log, if present)
grep "Failed password" /var/log/auth.log | grep -oE 'from [0-9.]+' | awk '{print $2}' \
  | sort | uniq -c | sort -rn | head -20

# top web attack sources (combined logs)
grep -hE '".*(\.env|\.git|wp-login|phpmyadmin|\.\./)' /var/www/httpd-logs/*.access.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

# queue and report journal
/usr/local/sbin/abuseipdb-report --stats

# current firewall bans per set
for s in $(nft list sets inet f2b-table | grep -oE 'addr-set-[a-z0-9-]+'); do
  echo "$s: $(nft list set inet f2b-table "$s" | grep -c 'elements')"
done

Diagnosis table

Symptom Likely cause Go to
fail2ban-client -t fails syntax, missing filter, bad jail file Troubleshooting
jail active, no bans filter matches nothing / threshold too high / ignoreip Step 4–5
bans in status, not in firewall ban action broken Step 6
bans in firewall, queue empty action not attached / fail2ban not restarted Step 3
queue has events, no reports key missing, API down, daily budget, throttle Troubleshooting
reports 401 key invalid Key Management
reports 429 per-IP or daily limit Report Anatomy
timer not running systemd/cron install failed systemctl status abuseipdb-report.timer

Rollback

fail2ban configuration

# the installer keeps a pristine copy of every file it replaced
ls /var/backups/fail2ban-abuseipdb/pristine-*/
# restore one file, e.g. the generated action/jail snippet
cp -a /var/backups/fail2ban-abuseipdb/pristine-<ts>/etc/fail2ban/jail.d/50-abuseipdb-reporter.local \
      /etc/fail2ban/jail.d/
rm -f /etc/fail2ban/jail.d/50-abuseipdb-reporter.local   # or remove the snippet entirely
fail2ban-client -t && systemctl restart fail2ban

For your own jails, restore the backup you took before editing and restart.

The project code

sudo /path/to/Fail2Ban_AbuseIPDB/install.sh --uninstall   # keeps config, queue, key
# or reinstall a known-good release archive with install.sh

A bad self-update

  • the updater restores the key snapshot if the installer fails and aborts before extraction on a checksum mismatch — in both cases nothing new is installed;
  • if a release proves bad after installation, restore files from /var/backups/fail2ban-abuseipdb/pristine-<ts>/ or reinstall the previous release archive;
  • key file, reporter.conf and the queue are independent of the code and survive a code rollback.

The queue

# inspect
/usr/local/sbin/abuseipdb-report --stats
# drop it (loses pending reports, not firewall state)
sudo systemctl stop abuseipdb-report.timer
sudo rm -f /var/lib/abuseipdb/abuseipdb.sqlite3*
sudo systemctl start abuseipdb-report.timer

The key

The key file is only read, never rewritten by updates. Restore it from your own secret store:

install -m 600 /dev/stdin /etc/abuseipdb/.abuseipdb.key <<< "KEY"
/usr/local/sbin/abuseipdb-report --check-key

Acceptance checklist

  • fail2ban-client -t is OK
  • all target jails are in fail2ban-client status
  • every target jail lists nftables, abuseipdb-async
  • each filter matches real abuse and ignores normal traffic
  • a documentation-IP test ban appears in nftables and can be unbanned
  • abuseipdb-report --stats shows the ban as pending
  • --dry-run shows the expected comment/categories
  • a real report returns 200
  • the report is visible through the AbuseIPDB check API
  • the timer/cron is active
  • backups exist (/var/backups/fail2ban-abuseipdb/, your own fail2ban backup)

Next: Agent Prompts: Fail2Ban Setup · Troubleshooting

Clone this wiki locally