Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Category:

Security

Published:

Tags:
  • fail2ban
  • ssh
  • linux
  • security
  • brute-force
  • systemd

How to Set Up Fail2ban to Stop SSH Brute-Force Attacks on Linux

Leave port 22 open to the internet for an afternoon and you will see failed login attempts within minutes. Almost none of it is targeted; it is botnets working through lists of IP ranges, trying "root" with a password list from some ten-year-old breach. Key-only authentication already stops these attacks from succeeding, but it does not stop them from happening, and a sufficiently persistent scanner will keep your auth log full of noise and your CPU quietly busy verifying handshakes it's always going to reject. Fail2ban's job is to notice the pattern and firewall the source out entirely, so it stops wasting your resources rather than just failing to authenticate against them.

This is a good complement to, not a replacement for, key-based SSH auth or a default-deny firewall. If you haven't set those up yet, do them first; fail2ban is a mitigation for volume, not a substitute for actual access control.

Installing fail2ban

On Debian and Ubuntu:

sudo apt update
sudo apt install fail2ban

On Fedora or RHEL-derivatives:

sudo dnf install fail2ban fail2ban-systemd

The fail2ban-systemd package matters on distributions that log exclusively to the systemd journal rather than a flat file like /var/log/auth.log, since fail2ban needs the journal backend to read entries in that case. Debian and Ubuntu ship both journald and a traditional syslog-style auth log by default (via rsyslog), so either backend works there; Arch and recent Fedora releases increasingly rely on journald alone.

Never edit the shipped config files

Fail2ban's own package installs /etc/fail2ban/jail.conf, and the file explicitly warns you not to edit it, because a package upgrade will overwrite it and silently discard your changes. Instead, create /etc/fail2ban/jail.local, which fail2ban reads afterwards and which will happily override anything in jail.conf. The same applies to filters and actions: if you need to customise one, copy it into the corresponding .d directory or a .local file rather than touching the original.

Configuring the SSH jail

Create /etc/fail2ban/jail.local with at least the following:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 4
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24

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

findtime and maxretry together define the trigger: four failures within a ten-minute window bans the source. bantime is how long the ban lasts. An hour is a reasonable starting point for a personal server; if you want repeat offenders to get progressively longer bans rather than a fixed hour every time, add:

bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 1w

The ignoreip line is the one people forget and then regret. Put your own static IP range or VPN subnet in there before you enable anything, not after you've locked yourself out. If you don't have a stable IP, consider whitelisting your WireGuard tunnel address instead and only allowing SSH over the tunnel in your firewall, which sidesteps the problem entirely.

Set backend = systemd explicitly on any distribution using journald, rather than leaving it on auto. auto usually gets it right, but when it doesn't, fail2ban fails quietly: the jail starts, reports as active, and simply never bans anyone, because it's watching a log file that nothing is writing to.

Starting it and checking it actually works

sudo systemctl enable --now fail2ban
sudo fail2ban-client status

That should list sshd under the jail list. Check the jail specifically:

sudo fail2ban-client status sshd

This shows the current failed and banned counts. To confirm the filter itself is matching your actual log format (which occasionally changes between OpenSSH versions), test it directly against the log:

sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

or, on a journald-only system:

sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

It will report how many lines matched. If it's zero despite failed logins existing in the log, the filter and your actual OpenSSH log format have drifted, which is worth knowing before you assume the jail is protecting you.

The honest way to test the whole pipeline is to attempt a handful of bad logins from a machine that isn't in ignoreip, and then check:

sudo fail2ban-client status sshd

You should see the IP move from "Currently failed" into "Banned IP list" after the fourth attempt. Unban it manually with:

sudo fail2ban-client set sshd unbanip 203.0.113.5

How the ban is actually enforced

Fail2ban doesn't do any packet filtering itself; it's a log-watching daemon that shells out to your firewall to add and remove rules. The default action on modern distributions is iptables-multiport or, increasingly, an nftables-based action, which inserts a rule into a dedicated fail2ban chain. If you're already running a hand-written default-deny nftables ruleset, check which action fail2ban is configured to use, because two independent tools editing firewall rules can interact in confusing ways, particularly around rule ordering and chain priorities. The cleanest approach is usually to let fail2ban manage its own chain and have your base ruleset jump into it early, rather than trying to merge the ban logic into your existing chains by hand.

You can confirm which backend is in use and see the actual rule it inserted:

sudo nft list chain inet filter f2b-sshd 2>/dev/null || sudo iptables -L f2b-sshd -n

Worth adding once the basics work

A few refinements are worth doing once the base jail is confirmed working, rather than upfront:

  • Email or webhook notification on ban, via the action directive, if you want to know when something is actively probing you rather than finding out from the ban list days later.
  • A permanent ban after repeated re-offences using the recidive jail, which watches fail2ban's own log for IPs that keep coming back after their ban expires and escalates them to a much longer or permanent block.
  • Extending the same approach to other exposed services: fail2ban ships filters for nginx auth failures, Postfix, Dovecot and others, and the jail mechanics are identical.

None of this replaces disabling password authentication in sshd_config and moving to keys or certificates, which removes the attack surface fail2ban is merely rationing. But for a box that has to keep SSH reachable from arbitrary IPs, it turns a log full of noise into a firewall that quietly does the filtering for you.