Scope of this guide
This article focuses on automatically blocking SSH brute-force attacks with fail2ban. Finish baseline setup first—switching to SSH key authentication and hardening sshd_config—using the Complete SSH Configuration and Access Control Guide.
Assumption: the SSH port has been changed to 2222 and only key authentication is allowed.
Installation and jail.local
The default jail.conf is overwritten on package updates, so always override in jail.local.
sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h # when
findtime = 10m # when failed
maxretry = 5 # when
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 # IP
backend = systemd # journald when using
[sshd]
enabled = true
port = 2222 # sshd_config when
maxretry = 3
bantime = 1dsudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd # jail checkWatch for port mismatch: If you changed the SSH port, set port under [sshd] to the same value so logs match. If they don't match, bans will not fire at all.
Core parameters — bantime · findtime · maxretry
| Parameter | Meaning | Recommended |
|---|---|---|
findtime | Observation window for counting failures | 10m |
maxretry | Ban after this many failures | SSH 3 |
bantime | How long a single ban lasts | 1d (escalate on repeats) |
bantime = -1 makes a permanent ban, but a false positive can lock you out, so incremental bans via the recidive jail below are safer.
Permanently banning repeat offenders — recidive jail
IPs that start hammering again as soon as a short ban expires get a long-term ban. This works by watching fail2ban's own logs.
# /etc/fail2ban/jail.local
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s #
bantime = 1w
findtime = 1d
maxretry = 5Email notification integration
To get mail when a ban occurs, switch action to a version that includes email.
[DEFAULT]
destemail = admin@example.com
sender = fail2ban@example.com
mta = sendmail
# + whois + to
action = %(action_mwl)sCloudflare and ipset integration — block at the edge
Blocking at the front (Cloudflare) or in the kernel with ipset reduces load and makes bans faster.
# Cloudflare API
[sshd]
action = cloudflare[cfuser="me@example.com", cftoken="API_TOKEN"]# or ipset + iptables ( IP O(1) )
[DEFAULT]
banaction = iptables-ipset-proto6-allportsOperations — status checks and manual ban/unban
# jail IP
sudo fail2ban-client status sshd
# IP /
sudo fail2ban-client set sshd banip 1.2.3.4
sudo fail2ban-client set sshd unbanip 1.2.3.4
# jail (settings after)
sudo fail2ban-client reloadMonitoring login attempts
Verify that ban rules are working and which IPs are knocking.
# failed when
journalctl -u ssh | grep "Failed password" | tail -20
# IP failed
journalctl -u ssh | grep "Failed password" \
| grep -oE "from [0-9.]+" | awk '{print $2}' | sort | uniq -c | sort -rn | head
# fail2ban
sudo zgrep "Ban " /var/log/fail2ban.log* | tail -20Avoid banning yourself: Always put your work static IP in ignoreip. If you omit it and ban yourself, you can only recover via console (web KVM). For baseline SSH hardening, see the Complete SSH Configuration and Access Control Guide.
AI tools may assist drafting. Check the evidence in each guide for whether commands were run and which environment was used. Confirm the current docs for your version before applying instructions, and email us about errors. email us
Questions & answers
Ask a question about this guide. We'll review and reply.