Any server with a public IP address receives a constant stream of automated login attempts against SSH and web login pages. Fail2Ban watches your logs for repeated authentication failures and temporarily blocks the offending IP addresses with firewall rules. In this tutorial you will install Fail2Ban on Ubuntu 24.04, protect SSH, add incremental bans for repeat offenders, protect an Nginx site with a custom filter, and learn how to inspect and lift bans.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - SSH access to the server. Know the public IP address you connect from, so you can whitelist it.
- Optional: Nginx installed, if you want to follow the web server steps.
WarningFail2Ban can ban your own IP address if you mistype your password several times. Keep a second way into the server (for example the VNC console in your provider's panel) until you have finished testing.
Step 1 - Installing Fail2Ban
Fail2Ban is available in the Ubuntu repositories. Install it together with python3-systemd, which lets Fail2Ban read the systemd journal directly instead of relying on text log files:
sudo apt update
sudo apt install fail2ban python3-systemd
Enable the service so it starts at boot, and start it now:
sudo systemctl enable --now fail2ban
Check that it is running:
sudo systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-24 10:12:03 UTC; 4s ago
Step 2 - Understanding the configuration files
Fail2Ban has three building blocks:
| Component | Location | Purpose |
|---|---|---|
| Filter | /etc/fail2ban/filter.d/*.conf | Regular expressions that detect a failed login in a log line |
| Action | /etc/fail2ban/action.d/*.conf | What to do with an offending IP, usually add a firewall rule |
| Jail | /etc/fail2ban/jail.conf, jail.d/, jail.local | Combines a filter, a log source and an action, plus the ban thresholds |
The file /etc/fail2ban/jail.conf holds the defaults and is overwritten on package upgrades, so never edit it. Fail2Ban reads files in this order, with later files overriding earlier ones:
/etc/fail2ban/jail.conf/etc/fail2ban/jail.d/*.conf(Ubuntu shipsdefaults-debian.confhere, which enables thesshdjail)/etc/fail2ban/jail.local/etc/fail2ban/jail.d/*.local
You will put all your settings in /etc/fail2ban/jail.local.
Step 3 - Setting global defaults and protecting SSH
Create the local configuration file:
sudo nano /etc/fail2ban/jail.local
Add the following content. Replace your_ip with the public IP address (or range) you administer the server from:
[DEFAULT]
# Never ban localhost or your own address
ignoreip = 127.0.0.1/8 ::1 your_ip
# Ban for 1 hour after 5 failures within 10 minutes
bantime = 1h
findtime = 10m
maxretry = 5
# Repeat offenders get longer bans: 1h, 2h, 4h... up to 1 week
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1w
[sshd]
enabled = true
backend = systemd
maxretry = 3
What these settings do:
ignoreip: addresses that are never banned. Add your office or VPN range here.bantime,findtime,maxretry: an IP that failsmaxretrytimes withinfindtimeis banned forbantime.bantime.increment: every new ban of the same IP lasts longer than the previous one, which is effective against slow, persistent bots.backend = systemd: reads SSH failures from the systemd journal, so the jail works even if rsyslog is not writing/var/log/auth.log.
If your SSH daemon listens on a non-standard port, add port = 2222 (with your port) to the [sshd] section. Otherwise the firewall rule blocks port 22 and the attacker keeps hitting the real port.
Test the configuration for syntax errors, then restart Fail2Ban:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
OK: configuration test is successful
Confirm that the sshd jail is active:
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 14
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 203.0.113.45
On a server that has been online for a while you will usually see failures and bans within minutes.
Step 4 - Banning repeat offenders with the recidive jail
bantime.increment only lengthens bans for an IP within one jail. The built-in recidive jail goes further: it reads Fail2Ban's own log and bans, on all ports, any IP that was banned several times by any jail.
Open jail.local again:
sudo nano /etc/fail2ban/jail.local
Append this section:
[recidive]
enabled = true
bantime = 1w
findtime = 1d
maxretry = 3
An IP banned three times in one day by any jail is now blocked for a week. Reload and check the list of jails:
sudo fail2ban-client reload
sudo fail2ban-client status
Status
|- Number of jail: 2
`- Jail list: recidive, sshd
Step 5 - Protecting Nginx
Fail2Ban ships filters for common Nginx abuse. Two useful ones are:
nginx-http-auth: failed HTTP basic authentication, read from the Nginx error log.nginx-botsearch: requests for common exploit paths that do not exist on your site.
Enable them in jail.local:
sudo nano /etc/fail2ban/jail.local
[nginx-http-auth]
enabled = true
[nginx-botsearch]
enabled = true
maxretry = 2
The default log paths (/var/log/nginx/error.log and /var/log/nginx/access.log) and ports (http,https) come from jail.conf, so you do not need to repeat them.
Creating a custom filter for WordPress logins
Application logins such as WordPress are not covered by a stock filter. Nginx logs every request, so you can match repeated POST requests to wp-login.php. Create the filter file:
sudo nano /etc/fail2ban/filter.d/nginx-wp-login.conf
[Definition]
failregex = ^<HOST> -.*"POST /wp-login\.php HTTP/[0-9.]+"
ignoreregex =
<HOST> is a Fail2Ban placeholder that captures the client IP address at the start of each line of the default Nginx access log format.
Before enabling the filter, test it against your real log with fail2ban-regex:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-login.conf
Results
=======
Failregex: 37 total
|- #) [# of hits] regular expression
| 1) [37] ^<HOST> -.*"POST /wp-login\.php HTTP/[0-9.]+"
`-
Lines: 5120 lines, 0 ignored, 37 matched, 5083 missed
If the filter matches the lines you expect, add the jail to jail.local. A legitimate user sends one POST per login, so five in ten minutes is a safe threshold:
[nginx-wp-login]
enabled = true
port = http,https
filter = nginx-wp-login
logpath = /var/log/nginx/access.log
maxretry = 5
Test and reload:
sudo fail2ban-client -t
sudo fail2ban-client reload
NoteIf your site is behind a reverse proxy or CDN, Nginx logs the proxy's IP address instead of the visitor's. Configure the Nginx
real_ipmodule first, or Fail2Ban will ban your proxy.
Step 6 - Managing bans
List all jails and their current bans:
sudo fail2ban-client status
sudo fail2ban-client status sshd
Unban an IP address from one jail:
sudo fail2ban-client set sshd unbanip 203.0.113.45
Unban an IP address from every jail at once:
sudo fail2ban-client unban 203.0.113.45
Ban an IP address manually, for example after spotting it in your logs:
sudo fail2ban-client set sshd banip 198.51.100.23
Fail2Ban implements bans as firewall rules in its own chains, named f2b-<jail>. You can see them with:
sudo iptables -L f2b-sshd -n
Chain f2b-sshd (1 references)
target prot opt source destination
REJECT all -- 203.0.113.45 0.0.0.0/0 reject-with icmp-port-unreachable
RETURN all -- 0.0.0.0/0 0.0.0.0/0
These rules coexist with UFW, so you can keep using UFW for your normal allow rules.
Fail2Ban logs every ban and unban to /var/log/fail2ban.log. To see today's bans:
sudo grep "Ban " /var/log/fail2ban.log | tail -n 20
2026-09-24 10:31:44,118 fail2ban.actions [8124]: NOTICE [sshd] Ban 203.0.113.45
2026-09-24 10:47:02,560 fail2ban.actions [8124]: NOTICE [nginx-botsearch] Ban 192.0.2.77
Step 7 - Testing a ban
Verify the whole chain works by triggering a ban from a machine whose IP is not in ignoreip, such as a different network or a second server. Attempt to log in with a non-existent user three times:
ssh wronguser@your_server_ip
After the third failure, new connections from that machine time out or are refused. On the server, the IP appears in the jail status:
sudo fail2ban-client status sshd
Unban the test machine when you are done:
sudo fail2ban-client set sshd unbanip test_machine_ip
Troubleshooting
The jail is enabled but never bans. Run fail2ban-regex against the log source the jail uses. For the systemd backend, test the journal directly:
sudo fail2ban-regex systemd-journal sshd
If nothing matches, the filter does not fit your log format or the jail reads the wrong file.
Fail2Ban fails to start after a change. Run sudo fail2ban-client -t to find the section with the error, and read the service logs:
sudo journalctl -u fail2ban -n 50 --no-pager
You locked yourself out. Connect through your provider's VNC console, then unban your IP with sudo fail2ban-client unban your_ip and add it to ignoreip.
Bans are applied but attacks continue. Check that the jail's port matches the port the service really listens on (sudo ss -tlnp).
Conclusion
Fail2Ban now watches SSH and Nginx, bans IPs that repeatedly fail to authenticate, and escalates bans for persistent attackers. Fail2Ban reduces noise and load, but it does not replace strong authentication. As next steps, disable SSH password authentication and use key-based logins only, restrict SSH to known IP ranges with UFW, and add jails for other exposed services such as Postfix or Dovecot using their stock filters.
