Fail2Ban reads log files, matches failed or abusive requests with regular expressions, and bans the offending IP addresses in the firewall for a set time. The default install only protects SSH. In this tutorial you will go further on Ubuntu 24.04: set global defaults with incremental ban times, harden the SSH jail, write and test a custom filter that catches scanners probing your Nginx site, reuse Nginx rate limiting as a ban trigger, and add a recidive jail that bans repeat offenders for a week.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, such as a CubePath VPS, with a non-root user that has sudo privileges.
  • SSH access from a fixed IP address, which you will add to the ignore list so you never lock yourself out.
  • Nginx installed and serving a site with the default combined access log format, for Steps 4 and 5. You can skip those steps if you only want to protect SSH.

Keep a second SSH session open while you work. If a rule bans your address, you can unban it from the other session.

Step 1 - Installing Fail2Ban

Fail2Ban is in the Ubuntu repositories. Install it:

sudo apt update
sudo apt install fail2ban

Enable and start the service:

sudo systemctl enable --now fail2ban

Check that it is running and which jails are active:

sudo fail2ban-client status
Status
|- Number of jail:	1
`- Jail list:	sshd

The sshd jail is enabled by the Ubuntu package in /etc/fail2ban/jail.d/defaults-debian.conf. Never edit /etc/fail2ban/jail.conf directly, because package upgrades overwrite it. All your settings go into /etc/fail2ban/jail.local and files in /etc/fail2ban/jail.d/, which are read after jail.conf and override it.

Step 2 - Setting global defaults and incremental bans

The [DEFAULT] section applies to every jail unless a jail overrides a value. Create /etc/fail2ban/jail.local:

sudo nano /etc/fail2ban/jail.local

Add the following, replacing your_admin_ip with the public IP address you connect from:

[DEFAULT]
# Never ban localhost or your own address
ignoreip = 127.0.0.1/8 ::1 your_admin_ip

# A host is banned after maxretry failures within findtime
findtime = 10m
maxretry = 5
bantime  = 1h

# Repeat offenders get longer bans: 1h, 2h, 4h ... up to 1 week
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 1w

What these options do:

  • ignoreip accepts single addresses, CIDR ranges and hostnames, separated by spaces.
  • findtime, bantime and maxtime accept suffixes such as m, h, d and w.
  • bantime.increment makes Fail2Ban look up previous bans of the same IP in its database (/var/lib/fail2ban/fail2ban.sqlite3) and multiply the ban time each time the address comes back.

Test the configuration before restarting. fail2ban-client -t parses every file and reports errors without touching the running service:

sudo fail2ban-client -t
OK: configuration test is successful

Restart Fail2Ban to apply the changes:

sudo systemctl restart fail2ban

Step 3 - Hardening the SSH jail

The stock sshd filter has several modes. The default normal mode matches failed passwords and invalid users. The aggressive mode also catches connections that close before authentication, key exchange errors and other patterns typical of scanners.

Create a dedicated file for the jail:

sudo nano /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled  = true
port     = ssh
mode     = aggressive
maxretry = 3
bantime  = 2h

If SSH listens on a non-standard port, set port to that number (for example port = 2222), otherwise the ban rule will block the wrong port.

Test and reload the configuration:

sudo fail2ban-client -t
sudo fail2ban-client reload

Check the jail:

sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:	2
|  |- Total failed:	41
|  `- File list:	/var/log/auth.log
`- Actions
   |- Currently banned:	3
   |- Total banned:	9
   `- Banned IP list:	198.51.100.23 203.0.113.54 192.0.2.77

Depending on the package defaults, the filter section may show Journal matches instead of File list, which means Fail2Ban reads SSH events from the systemd journal. Both work.

Step 4 - Writing a custom filter for Nginx scanners

Bots constantly probe web servers for files like .env, .git/config or wp-config.php. On a site that does not have these paths, every such request is a clear signal. You will write a filter that matches them in the Nginx access log.

A filter is a file in /etc/fail2ban/filter.d/ with a failregex that must contain the <HOST> tag, which Fail2Ban replaces with a pattern that captures the client IP. Create the filter:

sudo nano /etc/fail2ban/filter.d/nginx-probe.conf
[Definition]
failregex = ^<HOST> \S+ \S+ \[[^\]]+\] "(?:GET|POST|HEAD) /(?:\.env|\.git/|wp-config\.php|phpmyadmin|\.aws/|server-status)[^"]*" (?:403|404)

ignoreregex =

The regular expression matches a line from the combined log format: client IP, two fields, the timestamp in brackets, then a request for one of the probed paths answered with 403 or 404. Fail2Ban detects the Nginx timestamp format automatically.

Adjust the list of paths to your site. Do not include paths that your application actually serves, such as wp-login.php on a WordPress site.

Testing the filter with fail2ban-regex

Always test a filter against real logs before enabling it. First generate a matching request from another machine (not from your_admin_ip, which is ignored for bans but still logged):

curl -s -o /dev/null -w '%{http_code}\n' http://your_domain/.env
404

Then run fail2ban-regex on the server with the log file and the filter:

sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-probe.conf
Results
=======

Failregex: 1 total
|-  #) [# of hits] regular expression
|   1) [1] ^<HOST> \S+ \S+ \[[^\]]+\] "(?:GET|POST|HEAD) /(?:\.env|...

Date template hits:
|- [# of hits] date format
|  [1473] Day(?P<_sep>[-/])MON(?P=_sep)ExYear[ :]?24hour:Minute:Second(?:\.Microseconds)? Zone offset

Lines: 1473 lines, 0 ignored, 1 matched, 1472 missed

Check that matched is roughly what you expect and that the date template hit every line. Add --print-all-matched to see the exact lines that matched, which is the fastest way to spot false positives.

Enabling the jail

Create the jail file:

sudo nano /etc/fail2ban/jail.d/nginx-probe.local
[nginx-probe]
enabled  = true
port     = http,https
filter   = nginx-probe
logpath  = /var/log/nginx/access.log
backend  = auto
maxretry = 3
findtime = 10m
bantime  = 24h

backend = auto makes sure this jail reads the log file even if another configuration file sets the systemd journal as the default backend. If you have several virtual hosts with separate logs, list them all in logpath, one per line, or use a glob such as /var/log/nginx/*access.log.

Test and reload:

sudo fail2ban-client -t
sudo fail2ban-client reload
sudo fail2ban-client status nginx-probe

Step 5 - Banning clients that hit Nginx rate limits

Nginx can rate limit requests with limit_req, and every rejected request is written to the error log. Fail2Ban ships a filter called nginx-limit-req that matches those entries, so a client that keeps hammering the limit gets banned at the firewall instead of costing Nginx CPU time.

First define a rate limit zone in Nginx. Create a configuration file that is included inside the http block:

sudo nano /etc/nginx/conf.d/ratelimit.conf
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_status 429;

Apply the limit to the location you want to protect, for example a login endpoint, in your site configuration under /etc/nginx/sites-available/:

location /login {
    limit_req zone=perip burst=20 nodelay;
    # existing proxy_pass or try_files lines stay here
}

Test and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Now enable the jail for the stock filter:

sudo nano /etc/fail2ban/jail.d/nginx-limit-req.local
[nginx-limit-req]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/error.log
backend  = auto
maxretry = 10
findtime = 1m
bantime  = 1h

With these values, a client that is rejected by Nginx more than 10 times in one minute is banned for an hour. Reload Fail2Ban and confirm the jail is listed:

sudo fail2ban-client reload
sudo fail2ban-client status
Status
|- Number of jail:	3
`- Jail list:	nginx-limit-req, nginx-probe, sshd

Step 6 - Adding a recidive jail for repeat offenders

The recidive jail watches Fail2Ban's own log. When the same IP is banned several times by any jail, it is banned on all ports for a much longer period. The jail is already defined in jail.conf; you only need to enable it and tune it:

sudo nano /etc/fail2ban/jail.d/recidive.local
[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
banaction = %(banaction_allports)s
findtime = 1d
maxretry = 3
bantime  = 1w

Here an address banned three times in one day is blocked on every port for a week. bantime.increment from Step 2 keeps working on top of this, so persistent attackers stay out longer each time.

Reload and check:

sudo fail2ban-client reload
sudo fail2ban-client status recidive

Step 7 - Sending ban notifications with a custom action

Actions define what happens on ban and unban. The default action inserts a firewall rule; you can add a second action next to it. This example posts a message to a chat webhook (Discord format) whenever an IP is banned.

Make sure curl is installed:

sudo apt install curl

Create the action:

sudo nano /etc/fail2ban/action.d/webhook-notify.conf
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -fsS -m 5 -H "Content-Type: application/json" -d '{"content": "Fail2Ban on <fq-hostname>: banned <ip> in jail <name> after <failures> failures"}' "<webhook_url>"
actionunban =

[Init]
webhook_url =

<ip>, <name>, <failures> and <fq-hostname> are tags that Fail2Ban fills in at ban time. The -m 5 option makes sure a slow webhook never blocks the ban.

Attach the action to a jail in addition to the default ban action. For example, edit /etc/fail2ban/jail.d/sshd.local and add an action line, replacing the URL with your webhook:

[sshd]
enabled  = true
port     = ssh
mode     = aggressive
maxretry = 3
bantime  = 2h
action   = %(action_)s
           webhook-notify[webhook_url="https://discord.com/api/webhooks/your_webhook_id/your_webhook_token"]

%(action_)s is the standard "ban only" action defined in jail.conf. The indented second line adds your notification. Test and reload:

sudo fail2ban-client -t
sudo fail2ban-client reload

Step 8 - Managing bans

These are the commands you will use day to day.

List all currently banned IPs across jails:

sudo fail2ban-client banned

Ban an address manually in a given jail:

sudo fail2ban-client set sshd banip 203.0.113.54

Unban an address from one jail, or from every jail at once:

sudo fail2ban-client set sshd unbanip 203.0.113.54
sudo fail2ban-client unban 203.0.113.54

Follow Fail2Ban's log to see matches and bans as they happen:

sudo tail -f /var/log/fail2ban.log
2026-09-25 10:14:02,118 fail2ban.filter  [2314]: INFO    [nginx-probe] Found 198.51.100.23 - 2026-09-25 10:14:02
2026-09-25 10:14:05,402 fail2ban.actions [2314]: NOTICE  [nginx-probe] Ban 198.51.100.23

Bans survive a restart of the service: on startup Fail2Ban reads active bans from its SQLite database and restores them.

Troubleshooting

A jail fails to start with "Have not found any log file". The path in logpath does not exist. Check it with ls -l and fix the path, or set backend = systemd if the service logs only to the journal.

The filter matches in fail2ban-regex but nobody gets banned. Make sure the jail is listed in sudo fail2ban-client status, and that the source IP is not covered by ignoreip. Also check that maxretry failures actually happen within findtime.

Banned clients can still connect. Verify the port value of the jail matches the port the service listens on, and look for the ban rule with sudo nft list ruleset | grep -A5 f2b.

You locked yourself out. From the second session or the server console, run sudo fail2ban-client unban your_admin_ip and add the address to ignoreip.

Conclusion

Fail2Ban now protects SSH with a stricter filter, bans scanners and clients that abuse Nginx rate limits, escalates ban times for repeat offenders and can notify you on every ban. The same pattern works for any service that logs failures: write a filter, test it with fail2ban-regex, then enable a jail for it. As next steps, consider switching SSH to key-only authentication, adding jails for your mail or FTP services from the filters in /etc/fail2ban/filter.d/, and shipping /var/log/fail2ban.log to your central logging system.