Skip to content

Intrusion Prevention with Fail2ban

💡
Before you start

You need a Linux machine where you can use sudo, and you should not practise on a server anyone depends on. The commands here change real system state. A spare machine, a virtual machine, or a cloud instance you can rebuild is the right place; a laptop you own is fine too. If the terminal itself is new to you, do Introduction to the Linux Terminal first — it takes about ten minutes and everything below assumes it. Every step tells you how to undo it.

What Fail2ban Does

Fail2ban watches log files for patterns that indicate failure — repeated bad SSH logins, web authentication failures, mail relay attempts — and when one source crosses a threshold, it adds a firewall rule blocking that address for a while.

It is a noise reducer and a slow-down, not a wall. If you have already disabled password authentication on SSH, brute force cannot succeed anyway; Fail2ban then mainly keeps your logs readable and reduces wasted resources. That is still worth having, but it should be layered after proper authentication, never instead of it.

💡
Three concepts and you understand the whole tool.

A filter is a regex that recognises a failure in a log. A jail ties a filter to a log source, a threshold and a duration. An action is what happens on a match — normally inserting a firewall rule.

Install and Configure

sudo apt install fail2ban
systemctl status fail2ban
⚠️
Never edit jail.conf.

It is replaced on every package upgrade and your changes vanish. Create /etc/fail2ban/jail.local instead — it overrides jail.conf and is never overwritten. The same applies to individual jail files under jail.d/.

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
backend  = systemd

[sshd]
enabled = true
maxretry = 3
bantime = 1d
  • findtime — the window in which failures are counted
  • maxretry — failures within that window before a ban
  • bantime — how long the ban lasts. -1 is permanent, which builds an ever-growing rule set — prefer a long finite time
  • ignoreip — never ban these. Put your own network here
  • backend = systemd — read the journal rather than a log file. On current Debian and Ubuntu this is the correct setting, because /var/log/auth.log may not exist at all

Progressive banning punishes repeat offenders without affecting anyone else:

bantime.increment = true
bantime.factor = 2
bantime.maxtime = 5w
sudo systemctl restart fail2ban

Do Not Ban Yourself

This is the classic Fail2ban incident, and it is entirely avoidable.

  • Put your static address or LAN range in ignoreip before enabling anything
  • Have out-of-band access — a provider console or physical access — before you rely on this on a remote machine
  • Know the unban command in advance (below), so a mistake costs a minute
  • Remember dynamic addresses change — an ignoreip entry from home may not describe you tomorrow

Checking It Actually Works

A running service is not evidence that anything is being blocked. Verify the jail, then verify the firewall.

sudo fail2ban-client status                 # which jails are active
sudo fail2ban-client status sshd            # failures, bans, and current banned list

The sshd status output gives you the numbers that matter:

Filter
|  |- Currently failed: 2
|  `- Total failed:     417
Actions
   |- Currently banned: 3
   `- Total banned:     58

Then confirm the ban reached the firewall — this is the step people skip:

sudo iptables -S | grep f2b        # iptables backend
sudo nft list ruleset | grep f2b   # nftables backend

Manual control:

sudo fail2ban-client set sshd unbanip 203.0.113.9
sudo fail2ban-client set sshd banip 203.0.113.9
sudo fail2ban-client unban --all

Testing a Filter Before Trusting It

If you write or adapt a filter, test the regex against real log data rather than assuming it matches:

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

This reports how many lines matched. A filter that matches zero lines is a jail that will never ban anyone, and it will sit there looking perfectly healthy — the exact failure mode this command exists to catch.

Other Jails Worth Enabling

Only enable jails for services you actually run — a jail pointed at a log that does not exist just fails to start.

[nginx-http-auth]
enabled = true

[nginx-botsearch]
enabled = true

[postfix]
enabled = true

[recidive]
enabled  = true
bantime  = 4w
findtime = 1d
maxretry = 5

recidive is the useful meta-jail: it watches Fail2ban's own log and applies a long ban to addresses that keep getting banned by other jails.

💡
Fail2ban only sees IPv4 unless you tell it otherwise.

If your server has IPv6 connectivity, confirm the jails and actions handle it. A host reachable over IPv6 with IPv4-only banning is only half protected — and the attacker does not have to choose IPv4.

Now Do It Yourself: Five Steps

Fail2ban does one thing: it reads a log, matches a pattern, and bans the address it finds. Understanding it means understanding that pattern — so step 2 tests the matching for real, on real log lines, using nothing but grep. Get that right and the rest is configuration.

1
Find out whether it is installed, and whether it is actually running

Go: open a terminal on the server.

Do: run systemctl status fail2ban, then sudo fail2ban-client status.

You should see: the service reported as active (running), and then a list of jails — typically just sshd on a fresh install. Those are two different questions and both matter: a running daemon with no jails enabled protects nothing.

If not: Unit fail2ban.service could not be found means it is not installed — sudo apt install fail2ban on Debian and Ubuntu. If it is installed but inactive, start and enable it: sudo systemctl enable --now fail2ban. Installing alone does not start it, and starting alone does not survive a reboot.

2
See the actual mechanism — a pattern matched against a log line

Go: anywhere you can run grep. This step needs no fail2ban at all and every output below was captured for real.

Do: make a sample log line and test the core of fail2ban's sshd pattern against it.

printf 'Aug 20 03:11:09 host sshd[113]: Failed password for root from 198.51.100.4 port 40222 ssh2\n' > sample.log
grep -oE 'Failed password for (invalid user )?\S+ from [0-9.]+' sample.log

You should see: Failed password for root from 198.51.100.4. That is the whole idea — fail2ban's filter is a regular expression with a placeholder where the address sits, and the address it captures is the one it bans.

If not: 🔴 Try it with a capital P — grep -cE 'Failed Password for' sample.log returns 0, while the lowercase version returns a match. A filter that matches nothing produces no error, no warning and no bans; it simply sits there looking configured. That is the single most common way a fail2ban installation does nothing for months.

3
Configure a jail — in the right file

Go: on the server, create /etc/fail2ban/jail.local. 🔴 Do not edit jail.conf. It is replaced on every package upgrade and your changes vanish silently; jail.local overrides it and is never touched.

Do: put this in jail.local, replacing the address with your own, then reload with sudo systemctl reload fail2ban.

[DEFAULT]
ignoreip = 127.0.0.1/8 203.0.113.5
bantime  = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true

You should see: the reload complete without complaint. Read the three numbers together: 5 failures within 10 minutes earns a 1 hour ban. ignoreip is your seatbelt — put your own fixed address there so you cannot ban yourself.

If not: if the reload fails, the file has a syntax problem — section headers in square brackets, one key = value per line, no quotes. And if your own address is dynamic, ignoreip cannot save you; rely instead on key-only SSH, which never produces a failed password at all.

4
Confirm it is watching the right file

Go: same terminal.

Do: run sudo fail2ban-client status sshd.

You should see: the jail's file list, the number of failures it has seen, and the currently banned addresses. If the file list is empty or points somewhere that does not exist, the jail is running and reading nothing.

If not: the usual cause is a systemd-only machine with no /var/log/auth.log at all — there, the jail needs backend = systemd so it reads the journal instead of a file. Check whether that file exists before assuming the jail is broken. The exact wording of these fail2ban-client outputs was not captured while writing this page, because fail2ban is not installed on the machine it was written on. The commands and the configuration are the standard ones; step 2, which is the part that teaches the mechanism, was executed for real.

5
Learn how to unban before you need to

Go: same terminal. Do this now, while you still have access.

Do: run sudo fail2ban-client set sshd unbanip 198.51.100.4 to practise the command shape on an address that is not banned.

You should see: a numeric reply. Remember this command, because the day you need it you will be locked out and unable to look it up on the machine that has the answer.

If not: 🔴 If you ban yourself, fail2ban is not the problem — reaching the server is. Recovery means a console through your hosting provider, or physical access, exactly as with a firewall lockout. Three habits prevent it: put your address in ignoreip, use key-only SSH so a mistyped password cannot count against you, and keep a second session open whenever you change any of this.

🎉
Check yourself before moving on

Without scrolling up: fail2ban is installed, running, and has banned nobody in three months. Is that good news? Answer: not necessarily. It may mean nobody is attacking you, or it may mean the filter matches nothing at all — which produces no error. Check the failure count with fail2ban-client status sshd: zero failures seen on a public server means the jail is not reading what you think.

Now do it without the page: take three real lines from your own authentication log and test them against the pattern from step 2 with grep -oE. If your distribution words the message differently, adjust the pattern until it captures the address — that is exactly the work of writing a custom fail2ban filter.

Summary

  • Filter, jail, action — that is the whole model
  • Configure in jail.local, never jail.conf
  • backend = systemd on modern distros, where the log file may not exist
  • Set ignoreip before enabling and know the unban command in advance
  • Verify bans reach the firewall — a running service proves nothing
  • Layer it after key-only SSH, never instead of it
🎉
Prove it once.

Run sudo fail2ban-client status sshd and check that "Total banned" is greater than zero on an internet-facing host. If it is still zero after a day, the jail is not matching — run fail2ban-regex and find out why, rather than assuming you are simply unpopular.