fail2ban for SSH, then a recidive jail for repeat offenders

A short ban stops a password guesser for a while. A scanner that comes back an hour later and tries again gets a longer one. fail2ban can do both: a normal jail for SSH, and a recidive jail that watches fail2ban's own log for repeat bans.

Configure the SSH jail

On Ubuntu, sshd logs to the systemd journal, so the jail uses the systemd backend. Put the settings in /etc/fail2ban/jail.local, which overrides the packaged defaults.

ini
# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled  = true
backend  = systemd
port     = ssh
maxretry = 3

Add the recidive jail

recidive looks for Ban lines in /var/log/fail2ban.log. Five bans within a day earns a week, and the ban covers all ports.

ini
[recidive]
enabled   = true
logpath   = /var/log/fail2ban.log
bantime   = 1w
findtime  = 1d
maxretry  = 5

Reload and check

bash
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client status recidive

Count failed logins from the journal

To see the pressure before and after the change, count failed password attempts in the last hour:

bash
sudo journalctl -u ssh --since "1 hour ago" | grep -c "Failed password"

Rank the repeat offenders

This script reads fail2ban's log and ranks the addresses that were banned most often. It needs read access to /var/log/fail2ban.log.

python
import re
from collections import Counter
from pathlib import Path

BAN = re.compile(r"NOTICE\s+\[(?P<jail>[^\]]+)\] Ban (?P<ip>\S+)")


def bans(path: Path) -> Counter:
    counts: Counter = Counter()
    for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        match = BAN.search(line)
        if match:
            counts[(match["jail"], match["ip"])] += 1
    return counts


if __name__ == "__main__":
    for (jail, ip), n in bans(Path("/var/log/fail2ban.log")).most_common(10):
        print(f"{n:4}  {jail:10} {ip}")

Keep the bans honest

Ban lengths are a trade-off. An hour is enough for a guesser to give up. A week suits an address that keeps coming back. If a shared address ever gets caught, unban it with fail2ban-client set <jail> unbanip <ip> and check the list again tomorrow.

For the scrapers that ignore robots.txt, osec Bot Gate adds one more layer without moving your DNS.

Flow: failed logins, sshd jail, ban logged, comes back, recidive, review
Short bans first; repeat offenders get a week.

Sources

• fail2ban documentation: https://www.fail2ban.org/wiki/index.php/Main_Page
• fail2ban jail configuration reference (recidive): https://github.com/fail2ban/fail2ban/blob/master/config/jail.conf