Fail2ban watches login records and temporarily blocks an IP address in the firewall when it enters a wrong password again and again. You'll install it, set sensible rules including a harsher penalty for repeat attackers, and learn how to unblock an address if it happens to you.
What you'll need
- A StormHost VPS with Ubuntu 24.04 or Debian 13
- An SSH connection as root (guide)
- The browser console open in case you block yourself
- About 20 minutes
What fail2ban does#
Fail2ban works with jails. Each jail watches one service and has three parts:
| Part | What it does | Example for SSH |
|---|---|---|
| Filter | Recognizes a failed attempt in the records and extracts the IP address from it. | Lines Failed password for root from 198.51.100.25. |
| Log source (backend) | Where it reads the records from: the system journal or a file. | The SSH service journal. |
| Action (banaction) | What it does with an address that exceeds the limit. | Adds it to the blocked addresses in the firewall (nftables). |
When an address makes maxretry failures within findtime, fail2ban blocks it for bantime. When the time is up, it unblocks it by itself.
Fail2ban doesn't replace an SSH key
Fail2ban slows attackers down and keeps the logs clean, but it doesn't fully protect against a weak password being guessed. The best combination is an SSH key with passwords turned off plus fail2ban.
Installation#
This applies to both Ubuntu 24.04 and Debian 13.
Update the package list and install fail2ban:
bash apt update apt install fail2banfail2ban– the program itself.python3-systemdis installed as a dependency too, which lets it read the system journal.
Check that the
nfttool, which fail2ban uses for blocking, is available:bash command -v nft || apt install nftablescommand -v nft– prints the path to thenftprogram if it exists.||– the next command runs only if the previous one failed, meaning whennftis missing.apt install nftables– installs the tool. Installing it doesn't turn on any rules or change UFW.
Check that the service is running:
bash systemctl status fail2banLook for the line
Active: active (running). Quit the output with q.
Right after installation, fail2ban already protects SSH with the distribution's default settings: the sshd jail is on, reads records from the system journal and blocks through nftables. But the default ban is only 10 minutes, so adjust the settings.
Why jail.local and not jail.conf#
The default settings are in /etc/fail2ban/jail.conf and in /etc/. Don't edit these files; a package update could overwrite them. Your own settings go in /etc/fail2ban/jail.local. Fail2ban reads the files in the order jail.conf › jail.d/*.conf › jail.local › jail.d/*.local, and a later value always overrides an earlier one.
Configuration line by line#
Create the file:
bash nano /etc/fail2ban/jail.localPut this in it:
/etc/ fail2ban/ inijail.local [DEFAULT] # Addresses that are never banned (separated by spaces) ignoreip = 127.0.0.1/8 ::1 # Length of the first ban bantime = 1h # Window in which failed attempts are counted findtime = 10m # Number of failed attempts that triggers a ban maxretry = 5 # Ban repeat offenders for longer bantime.increment = true bantime.factor = 1 bantime.maxtime = 1w [sshd] enabled = true port = ssh backend = systemd [recidive] enabled = true backend = auto logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 1w findtime = 1d maxretry = 5Save (Ctrl+O, Enter) and close the editor (Ctrl+X).
Sections in square brackets: [DEFAULT] applies to all jails; [sshd] and [recidive] apply only to that jail and take precedence over [DEFAULT].
| Setting | What it does | Recommended value | When to change |
|---|---|---|---|
ignoreip | Addresses and ranges that fail2ban never blocks. Separated by spaces. 127.0.0.1/8 ::1 is the VPS itself. | 127.0.0.1/8 ::1 | Add your IP address only if it's static (an office, your own server). Don't add the dynamic address of a home connection; after a while someone else may get it. |
bantime | How long an address stays blocked. Units: m minutes, h hours, d days, w weeks. The default is 10m. | 1h | Shorter (30m) if you often mistype and don't have the console at hand. |
findtime | The time window in which failures are counted. | 10m | Longer (1h) against slow attacks that try a password once every few minutes. |
maxretry | How many failures within findtime lead to a block. With 3 you can block yourself with a typo; with 10 an attacker gets more attempts. | 5 | 3 for SSH with passwords turned off (a legitimate user with a key doesn't make mistakes). |
bantime.increment | Each further block of the same address lasts longer. Fail2ban keeps the history in a database. | true | false if you always want the same time. |
bantime.factor | The growth multiplier. With the value 1 the time grows 1, 2, 4, 8… × bantime. | 1 | Higher (2) for faster growth. |
bantime.maxtime | The cap beyond which the block time won't grow. | 1w | Longer (4w) for very persistent attackers. |
enabled | Turns the jail on. Jails are off by default. | true | false turns the jail off temporarily. |
port | Which ports are blocked for the address. ssh means port 22. | ssh | If SSH runs on a different port, write it as a number, for example 2222 (see below). |
backend | Where to read records from. systemd = the system journal, auto = the file from logpath. | systemd for sshd, auto for recidive | Never, unless you know why. Debian 13 has no /var/log/auth.log file, so SSH has to read the journal. |
logpath | The file with the records (only for backend = auto). | /var/log/fail2ban.log | Doesn't change. |
banaction | How to block. %(banaction_allports)s = block all ports, not just SSH. | %(banaction_allports)s | Doesn't change. |
Recidive: a harsher penalty for repeat attackers#
The recidive jail reads fail2ban's own records (/var/log/fail2ban.log). When the same address gets at least maxretry (5) blocks in any jail within findtime (1 day), recidive blocks it for bantime (1 week) and on all ports. An attacker who keeps coming back thus loses access to the VPS completely.
That's why recidive reads a file (backend = auto) and not the journal: fail2ban writes its records to the file /var/log/fail2ban.log, not to the journal.
For fail2ban to remember the block history long enough (for bantime.increment and for restoring week-long blocks after a restart), extend how long it's kept in the database:
nano /etc/fail2ban/fail2ban.local[Definition]
dbpurgeage = 8d| Setting | What it does | Recommended value | When to change |
|---|---|---|---|
dbpurgeage | After how long old block records are deleted from the database /var/. The default is 1d. | 8d | Always a bit longer than the longest bantime and bantime.maxtime. |
Starting and checking#
Check the configuration:
bash fail2ban-client -t-t– configuration test. On success it printsOK: configuration test is successful. On an error it names the file and the problem.
Restart fail2ban so it loads the new settings:
bash systemctl restart fail2banList the active jails:
bash fail2ban-client statusOutputtext Status |- Number of jail: 2 `- Jail list: recidive, sshdShow the status of the
sshdjail:bash fail2ban-client status sshdOutputtext Status for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 57 | `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd `- Actions |- Currently banned: 3 |- Total banned: 12 `- Banned IP list: 198.51.100.25 198.51.100.40 198.51.100.77
| Line | Meaning |
|---|---|
Currently failed | How many addresses currently have at least one failure within the findtime window but aren't blocked yet. |
Total failed | The number of failures caught since fail2ban started. |
Journal matches | Which journal records the jail reads. On Ubuntu it may differ (sshd.service); this doesn't affect how it works. |
Currently banned | How many addresses are blocked right now. |
Total banned | How many blocks have happened since the start. |
Banned IP list | The list of currently blocked addresses. |
Unblocking an IP address#
If you've blocked yourself, log in through the browser console (or from a different network, for example over mobile data) and use one of these commands:
| Command | What it does |
|---|---|
fail2ban-client set sshd unbanip 198.51.100.25 | Unblocks the address in the sshd jail. |
fail2ban-client unban 198.51.100.25 | Unblocks the address in all jails (including recidive) and deletes it from the database. |
fail2ban-client unban --all | Unblocks all addresses in all jails. Attackers get blocked again on further attempts. |
fail2ban-client banned 198.51.100.25 | Shows which jails the address is blocked in. |
Replace 198.51.100.25 with your public IP address. If you don't know it, find it in the Banned IP list by time (block records: grep "Ban " /var/log/fail2ban.log | tail), or open any “what is my IP address” page on the computer you connect from.
You can also find your address while you're logged in over SSH: echo $SSH_CLIENT prints the address you're connecting from as the first value.
How to tell that you're the one blocked#
- SSH suddenly reports
Connection refusedorConnection timed out, although it worked a moment ago. - From a different network (mobile data) you connect without a problem.
- If
recidiveblocked you, other services on the VPS (website, game server) don't work for you either, while they work for other people. - In the console you see your address in the output of
fail2ban-client status sshd.
The customer area and password changes
The Files and Database tabs log in to the VPS over SSH with a password. When you change the root password and the tabs try to log in with the old one several times, fail2ban may also block the address the customer area connects from. So right after changing the password, enter the new one in Files › Change credentials. If the tab already doesn't work, run fail2ban-client unban --all and enter the credentials again.
SSH on a different port#
If you moved SSH (for example to 2222), update the line in the [sshd] section of jail.local:
[sshd]
enabled = true
port = 2222
backend = systemdport = 2222– fail2ban blocks this port for the attacker. With the wrong port, a block would have no effect. For more ports, separate them with a comma:port = 22,2222.
Then run fail2ban-client -t and systemctl restart fail2ban.
More jails: Nginx#
If you run a website with Nginx on the VPS, you can watch it too. Add to jail.local:
[nginx-http-auth]
enabled = true
backend = auto
[nginx-botsearch]
enabled = true
backend = auto| Jail | What it watches |
|---|---|
nginx-http-auth | Wrong passwords on password-protected pages (HTTP Basic Auth). |
nginx-botsearch | Bots looking for known vulnerable addresses (/wp-login.php, /phpmyadmin and so on). |
backend = auto– Nginx writes to the file/var/log/nginx/error.log, not to the journal. Without this line, on Ubuntu the jail would read the journal and find nothing.
Fail2ban and the firewall#
Fail2ban blocks through nftables in its own table f2b-table. It therefore works alongside UFW and the firewall in the customer area and doesn't overwrite any of them. You'll see the blocked addresses with:
nft list table inet f2b-tablenft list table inet f2b-table– prints fail2ban's table including the sets (addr-set-sshd…) with the blocked addresses.
If you want to see the blocked addresses right in ufw status, set the line banaction = ufw in [DEFAULT] (UFW must be on). Then set banaction = ufw for the recidive jail too. The default nftables is faster, though, and we recommend it.
Fail2ban protects only what runs inside the VPS. It doesn't control the firewall in the customer area.
Verification#
Try blocking a test address by hand:
bash fail2ban-client set sshd banip 198.51.100.25Check that it's in the firewall:
nft list table inet f2b-tablemust contain the address198.51.100.25.Unblock it:
fail2ban-client set sshd unbanip 198.51.100.25.After an hour or two, check
fail2ban-client status sshd. On a public VPS,Total failedandTotal bannedare usually non-zero soon.
Troubleshooting#
fail2ban-client -t reports an error#
The output names the file and section. Most often it's a typo in a setting name, a missing =, or a value without a unit. Fix the file and run the test again.
Have not found any log file for sshd jail#
The sshd jail is trying to read the file /var/log/auth.log, which doesn't exist on Debian 13. The [sshd] section must contain backend = systemd.
No module named 'asynchat' (Ubuntu 24.04)#
The original fail2ban version in Ubuntu 24.04 didn't work with Python 3.12. The fix is in the distribution updates: run apt update && apt full-upgrade and then systemctl restart fail2ban.
Fail2ban reports addresses as blocked, but the attacker keeps trying passwords#
- Look for action errors in
/var/log/fail2ban.log(for examplenft: not found). Then install thenftablespackage. - Check that the
portin the jail matches the actual SSH port.
Where to look for details#
tail -n 50 /var/log/fail2ban.log
journalctl -u fail2ban -n 50tail -n 50 /var/log/fail2ban.log– the last 50 lines of the fail2ban log (blocksBan, unblocksUnban, errors).journalctl -u fail2ban -n 50– the last 50 service entries; useful when fail2ban doesn't start at all.
Summary
- Fail2ban blocks IP addresses after repeated failed logins; install it with
apt install fail2ban. - Your own settings go in
/etc/fail2ban/jail.local, never injail.conf. - SSH reads the journal (
backend = systemd),recidivereads a file (backend = auto). - Unblocking:
fail2ban-client set sshd unbanip <IP>orfail2ban-client unban <IP>; if you lock yourself out, use the console. - After moving SSH, update the
portin thesshdjail.