Skip to content

Fail2ban Intrusion Prevention

💡
Before you start

You need a Linux terminal and Python 3 (python3 --version). The first two steps build Fail2ban’s core logic yourself and need nothing special; the ban step uses iptables, which needs sudo. To run the real daemon, install it with sudo apt install fail2ban.

Fail2ban runs as a background service under systemd, which this build environment does not provide, so its ban action below was reproduced directly with iptables in a throwaway network namespace — the exact command Fail2ban issues. On a normal server the daemon does all of this for you; the point of the lab is to see what “for you” actually means. The banned address used throughout, 203.0.113.7, is a reserved documentation IP — banning it affects nothing real.

What is Fail2ban?

Fail2ban is an intrusion prevention tool that monitors log files for suspicious activity (like repeated failed login attempts) and automatically bans offending IP addresses by updating firewall rules.

It is one of the most effective defenses against brute-force attacks on SSH, web servers, mail servers, and other services.

💡
How it works

Fail2ban watches log files → detects patterns of failure → adds a temporary firewall rule to block the attacker's IP → automatically unbans after a set time.

Installation

On Ubuntu/Debian:

sudo apt update
sudo apt install fail2ban

Start and enable the service:

sudo systemctl start fail2ban
sudo systemctl enable fail2ban

Verify it is running:

sudo systemctl status fail2ban

Configuration: jail.local

Never edit /etc/fail2ban/jail.conf directly as it gets overwritten on updates. Instead, create a local override file:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Key Parameters

bantime How long an IP stays banned (default: 10m). Use bantime = 1h for one hour.
findtime The time window to count failures (default: 10m). If maxretry failures happen within findtime, the IP is banned.
maxretry Number of failures before banning (default: 5).

Enabling the SSH Jail

Find the [sshd] section in jail.local and ensure it is enabled:

[sshd]
enabled = true
port    = ssh
logpath = %(sshd_log)s
maxretry = 5
bantime  = 1h
findtime = 10m

After changes, restart Fail2ban:

sudo systemctl restart fail2ban

Monitoring Fail2ban

Check overall status:

sudo fail2ban-client status

Check a specific jail:

sudo fail2ban-client status sshd

Example output:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     47
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     8
   `- Banned IP list:   203.0.113.50

Unbanning an IP

If you accidentally ban a legitimate IP (like your own):

sudo fail2ban-client set sshd unbanip 203.0.113.50
⚠️
Do not lock yourself out

Before enabling Fail2ban on SSH, make sure you have console access or an alternative way to reach the server in case you accidentally trigger a ban on your own IP.

Testing Fail2ban

To verify Fail2ban is working, you can intentionally trigger failed logins from a test IP:

# From another machine, attempt SSH with wrong credentials
ssh baduser@your-server-ip
# Repeat until maxretry is exceeded

Then check if the IP was banned:

sudo fail2ban-client status sshd

Now Do It Yourself: Ban a Brute-Forcer in Five Steps

Fail2ban watches a log, counts failures from each address, and after too many it tells the firewall to drop that address. You will do each of those by hand — read the attack out of a log, write the detector that counts it, issue the exact firewall ban Fail2ban issues — and then set up the real daemon to do it automatically. The Python and iptables output below was produced by running it.

1
See the attack in the log

Go: a terminal. Real SSH failures are logged to /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (Fedora).

Do: look at the failed-login lines. On a live server: sudo grep 'Failed password' /var/log/auth.log | tail. To follow along with fixed data, save this sample as auth.log:

Aug 21 10:15:02 web sshd[2011]: Failed password for root from 203.0.113.7 port 51900 ssh2
Aug 21 10:15:05 web sshd[2013]: Failed password for root from 203.0.113.7 port 51902 ssh2
Aug 21 10:15:08 web sshd[2015]: Failed password for invalid user admin from 203.0.113.7 port 51904 ssh2
Aug 21 10:15:40 web sshd[2020]: Accepted password for alice from 198.51.100.24 port 40122 ssh2
Aug 21 10:16:01 web sshd[2031]: Failed password for root from 192.0.2.55 port 33001 ssh2
Aug 21 10:16:07 web sshd[2033]: Failed password for invalid user oracle from 203.0.113.7 port 51950 ssh2

You should see: one IP (203.0.113.7) hammering the login again and again, mixed in with a single legitimate Accepted password. That pattern — many failures from one source in a short time — is exactly what Fail2ban is built to spot.

If not: on a real server with no attacks yet you may see few or no failures — that is good news, and the sample above lets you continue regardless.

2
Write the detector: count failures per IP

Go: the folder holding your auth.log.

Do: save this as detect.py and run python3 detect.py. The regular expression is the same shape as Fail2ban’s built-in sshd filter.

import re
from collections import Counter

failregex = re.compile(r"Failed password for (?:invalid user )?\S+ from (?P<ip>\d+\.\d+\.\d+\.\d+)")
fails = Counter()
for line in open("auth.log"):
    m = failregex.search(line)
    if m:
        fails[m.group("ip")] += 1

print("failed logins per IP:")
for ip, n in fails.most_common():
    print("  %-15s %d" % (ip, n))

MAXRETRY = 3
print("\nover the limit (maxretry=%d) -> BAN:" % MAXRETRY)
for ip, n in fails.items():
    if n >= MAXRETRY:
        print("  ", ip)

You should see: the attacker counted and singled out, while the one-off and the successful login are left alone:

failed logins per IP:
  203.0.113.7     4
  192.0.2.55      1

over the limit (maxretry=3) -> BAN:
   203.0.113.7

Notice the regex never matched the Accepted password line — that is why a good filter matters: ban on the wrong pattern and you lock out real users.

If not: if every count is zero, the file is not being read — confirm auth.log is in the current folder and the failed-login lines are intact.

3
Issue the ban Fail2ban issues

Go: the same terminal. When Fail2ban decides to ban, its default action adds one firewall rule. You will run that exact rule for the flagged IP. It blocks only 203.0.113.7, so it cannot lock you out.

Do: insert a DROP rule for the attacker, confirm it is there, then remove it (which is what an unban does).

sudo iptables -I INPUT -s 203.0.113.7 -j DROP
sudo iptables -S INPUT | grep 203.0.113.7
sudo iptables -D INPUT -s 203.0.113.7 -j DROP

You should see: the ban rule appear, then vanish:

-A INPUT -s 203.0.113.7/32 -j DROP

While that rule is in place, every packet from 203.0.113.7 is silently dropped — the brute-force stops dead. That single line is the ban; everything else Fail2ban does is deciding when to add it and when to take it away.

If not: iptables: command not found on some systems means the newer nft is in use — sudo nft add rule inet filter input ip saddr 203.0.113.7 drop is the equivalent, and Fail2ban picks the right backend for you.

4
Let the real daemon do all three automatically

Go: with Fail2ban installed, create /etc/fail2ban/jail.local with sudo (never edit jail.conf — upgrades overwrite it).

Do: enable the SSH jail with the same numbers you used by hand.

[sshd]
enabled  = true
maxretry = 3
findtime = 10m
bantime  = 1h

Then reload: sudo systemctl restart fail2ban.

You should see: Fail2ban now watches auth.log continuously, and any IP with maxretry failures inside findtime gets the step-3 rule added for bantime, then removed automatically. You configured the three numbers; it does the reading, counting, and banning forever.

If not: systemctl status fail2ban showing failed usually means a typo in jail.local or a filter that does not match your log format — sudo fail2ban-client -d dumps the parsed config so you can see what it actually loaded.

5
Watch it work, and free an IP you banned by mistake

Go: the terminal on the server running Fail2ban.

Do: check the jail’s status, and if you ever ban yourself from a fat-fingered password, release your own address.

sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 203.0.113.7

You should see: a status block listing Currently banned IPs and the total ban count; the unbanip command removes exactly the iptables rule you saw in step 3. This is the escape hatch every admin needs, because Fail2ban will happily ban you if you mistype your password too many times.

If not: unbanip reporting the IP was not banned just means it is not currently on the list — check the exact address from the status output first. To avoid self-bans, add your own admin IP to ignoreip in jail.local.

🎉
Check yourself before moving on

Without scrolling up: your jail has maxretry = 3 and findtime = 10m. An attacker makes 2 failed attempts, waits fifteen minutes, makes 2 more, waits again, and repeats all night. Do they get banned? Answer: no. Fail2ban only counts failures within the findtime window, and they never reach 3 inside any 10-minute span. This “low and slow” attack is the known weakness of failure-counting; defences are a longer findtime, a smaller maxretry, or better, key-only SSH so passwords cannot be brute-forced at all.

Now do it without the page: extend your detect.py to also respect a time window — parse the timestamp on each line and only count failures whose times fall within findtime of each other. You will have rebuilt the single most important idea in Fail2ban, and you will understand exactly why the low-and-slow attack slips past it.

Summary

In this tutorial, you learned:

  • What Fail2ban does and how it protects your server
  • How to install and enable Fail2ban
  • Configuring jail.local with bantime, findtime, and maxretry
  • Monitoring banned IPs and jail status
  • How to unban IPs and test your configuration
🎉
Your server is now protected!

Fail2ban will automatically block brute-force attackers. Check the logs periodically to see how many attacks it is stopping.