Forensic queries for fail2ban: who got banned and why
code

Forensic queries for fail2ban: who got banned and why

Auditing the probers, lurkers, and peepers

The Lad 4 min read
Read More
Older Post
🧑‍💻
TL; DR for quick forensics:
sudo fail2ban-client status nginx-botsearch
why-banned <ip_address_in_question>

If you run fail2ban in front of a public-facing service, you'll eventually want to answer questions like "why was this IP banned?", "what did they try?", or "which scanners are hitting me hardest this week?". The good news: fail2ban already logs everything you need. You just have to know how to ask.

This post collects a handful of one-liners I keep coming back to when investigating ban activity on an nginx server. They assume a setup with fail2ban tailing nginx access logs, but the patterns work for any jail.

💡
IPs in the post use 198.51.100.0/24 (TEST-NET-2)&& 203.0.113.0/24 (TEST-NET-3) which is an RFC 5737 documentation address (reserved, can never be a real server IP — same idea as 192.168.x.x but for examples).

The basic workflow

When you see a banned IP and want to know what it did, you need two pieces of information:

  1. fail2ban's view: when did the filter match, and when did the ban fire?
  2. nginx's view: what requests did that IP actually make?

fail2ban writes its activity to /var/log/fail2ban.log. Each match gets a Found line with a timestamp, and each ban gets a Ban line. Cross-referencing those timestamps against your access log tells you exactly which requests triggered the action.

Show recent bans with reasons

The simplest query: what's been happening lately?

sudo grep -E "Ban |Found " /var/log/fail2ban.log | tail -20

This shows the last 20 events across all jails. You'll see lines like:

[nginx-botsearch] Found 198.51.100.42 - 2026-05-20 15:07:04
[nginx-botsearch] Ban 198.51.100.42

The Found line is the filter matching a log entry. The Ban line is fail2ban actually taking action (which happens once maxretry is exceeded within findtime).

Show all activity for a specific IP

Once you have an IP of interest, pull everything related to it from both logs:

sudo grep "198.51.100.42" /var/log/fail2ban.log /var/log/nginx-proxy/access.log*

You'll get the full story: each fail2ban event, plus every request that IP made (across rotated access logs too, since the * glob picks up access.log.1, etc.).

For compressed rotated logs, add a zgrep pass:

sudo zgrep "198.51.100.42" /var/log/nginx-proxy/access.log.*.gz

Count bans by IP

Want to know which scanners are the most persistent? Aggregate by IP and sort by frequency:

sudo grep "Ban " /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20

Output looks like:

      4 198.51.100.42
      3 198.51.100.43
      2 203.0.113.47
      1 203.0.113.182

The number on the left is how many times that IP has been banned. Repeat offenders show up at the top — useful if you want to consider a longer ban time for chronic abusers (or feed them into a recidive jail).

Currently banned IPs across all jails

fail2ban exposes a one-shot summary of who's currently in the doghouse:

sudo fail2ban-client banned

This shows banned IPs grouped by jail. Much faster than grepping the log when you just want to know "is this IP currently blocked?"

For per-jail detail (including filter stats and recent ban history):

sudo fail2ban-client status nginx-botsearch

Quality-of-life: a "why-banned" script

If you find yourself running the cross-reference query often, drop it in a script:

sudo tee /usr/local/bin/why-banned > /dev/null <<'EOF'
#!/bin/bash
# Usage: why-banned <ip>
ip="$1"
if [ -z "$ip" ]; then
    echo "Usage: why-banned <ip>"
    exit 1
fi
echo "=== fail2ban events ==="
sudo grep "$ip" /var/log/fail2ban.log
echo
echo "=== nginx access entries ==="
sudo grep "$ip" /var/log/nginx-proxy/access.log* 2>/dev/null
sudo zgrep "$ip" /var/log/nginx-proxy/access.log.*.gz 2>/dev/null
EOF
sudo chmod +x /usr/local/bin/why-banned

Then any time you see a banned IP and wonder what happened:

why-banned 198.51.100.42

You get the full story in one shot: every fail2ban event for that IP, plus every request it made across both live and rotated logs.

Aliases worth saving

For interactive use, add these to ~/.bashrc or ~/.zshrc:

alias f2b-recent='sudo grep -E "Ban |Found " /var/log/fail2ban.log | tail -20'
alias f2b-top='sudo grep "Ban " /var/log/fail2ban.log | awk "{print \$NF}" | sort | uniq -c | sort -rn | head -20'
alias f2b-banned='sudo fail2ban-client banned'

Adjust paths to your setup

The commands above assume:

  • fail2ban logs to /var/log/fail2ban.log (the default on Debian/Ubuntu)
  • nginx access logs are at /var/log/nginx-proxy/access.log with rotated copies in the same directory

If your paths differ (e.g., /var/log/nginx/access.log for a non-containerized nginx), substitute accordingly. The patterns themselves don't change.

Why this matters

Bans aren't fire-and-forget. Periodically reviewing what's getting banned helps you spot:

  • New attack patterns that your current filters might be missing
  • False positives hitting legitimate users or crawlers
  • Persistent threats worth escalating to a longer ban time or a permanent block
  • Filter misconfiguration — if you see zero bans over a week and you know scanners are hitting you, something's broken

The data has been there the whole time. These queries just make it easy to surface.