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.
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 = 1h for one hour.
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
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.
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.
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.
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.
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.
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.
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
Fail2ban will automatically block brute-force attackers. Check the logs periodically to see how many attacks it is stopping.