Install and Configure Fail2Ban on a Linux VPS
Set up Fail2Ban to block brute-force attacks on SSH and Nginx. Covers UFW and nftables ban actions, custom jails, recidive escalation, and filter testing on Ubuntu 24.04 and Debian 12.
Fail2Ban watches your log files for repeated authentication failures and bans the offending IP addresses through your firewall. It stops brute-force attacks before they succeed. A freshly deployed VPS typically sees SSH login attempts within minutes of going live. Fail2Ban is the standard defense.
This guide covers installation on Ubuntu 24.04 and Debian 12, SSH and Nginx jail configuration, both UFW and nftables ban action backends, the recidive jail for repeat offenders, and filter testing with fail2ban-regex. Every configuration change includes a verification step.
Prerequisites: A VPS running Ubuntu 24.04 or Debian 12 with root or sudo access. Your firewall should already be active () and SSH should be hardened (). This guide is part of the series.
How do I install Fail2Ban on Ubuntu 24.04 and Debian 12?
Install Fail2Ban with apt. The package is in the default repositories for both distributions. Ubuntu 24.04 ships version 1.0.2, Debian 12 ships version 1.0.2. On Debian 12, you also need python3-systemd so Fail2Ban can read the systemd journal.
sudo apt update && sudo apt install -y fail2ban
On Debian 12, install the systemd Python bindings:
sudo apt install -y python3-systemd
Enable and start the service:
sudo systemctl enable --now fail2ban
The enable flag makes Fail2Ban start on boot. The --now flag starts it immediately. Verify it is running:
sudo systemctl status fail2ban
The output shows Active: active (running). If the status shows failed, check the journal:
journalctl -u fail2ban -n 20 --no-pager
What is the difference between jail.conf, jail.local, and jail.d/?
jail.conf is the default configuration file shipped with the package. Package updates overwrite it. Never edit jail.conf directly. Your changes will disappear on the next apt upgrade.
jail.local is the traditional override file. Fail2Ban reads jail.conf first, then applies any settings from jail.local on top. This works, but the file grows into a monolith that is hard to maintain.
The jail.d/ directory is the better approach. Drop one .conf file per jail. Fail2Ban loads them alphabetically after jail.conf and jail.local. This keeps each service isolated and makes it easy to add or remove jails without touching other configurations.
This guide uses jail.d/ drop-in files. Create the directory if it does not exist:
ls /etc/fail2ban/jail.d/
It should already exist. If you see files like defaults-debian.conf, that is normal. On Ubuntu 24.04, this file enables the sshd jail and sets banaction = nftables as the default ban action. On Debian 12, it enables the sshd jail. Because Fail2Ban loads jail.d/ files in alphabetical order, any custom defaults file must sort after defaults-debian.conf to override its settings. This guide uses zz-defaults.conf for that reason.
How do I configure the SSH jail in Fail2Ban?
The SSH jail monitors authentication logs and bans IPs that fail login too many times. On both Ubuntu 24.04 and Debian 12, the sshd jail is enabled by default through /etc/fail2ban/jail.d/defaults-debian.conf. But the defaults are loose (5 retries, 10-minute ban). Tighten them.
Create a drop-in configuration:
sudo tee /etc/fail2ban/jail.d/sshd.conf > /dev/null << 'EOF'
[sshd]
enabled = true
mode = aggressive
port = ssh
backend = systemd
maxretry = 3
findtime = 600
bantime = 3600
EOF
What each setting does:
| Parameter | Value | Meaning |
|---|---|---|
enabled |
true |
Activate this jail |
mode |
aggressive |
Match more SSH failure patterns including key auth failures |
port |
ssh |
Monitor the SSH port (resolves to 22, or your custom port) |
backend |
systemd |
Read from the systemd journal instead of log files |
maxretry |
3 |
Ban after 3 failed attempts |
findtime |
600 |
Count failures within a 10-minute window |
bantime |
3600 |
Ban for 1 hour (3600 seconds) |
If you changed your SSH port (you should), replace ssh with your port number:
port = 2222
Restart Fail2Ban to apply:
sudo systemctl restart fail2ban
Verify the jail is active:
sudo fail2ban-client status sshd
Expected output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
The Journal matches line shows Fail2Ban reads from the systemd journal, not a log file. This is correct for Ubuntu 24.04 and Debian 12.
How do I set up Fail2Ban with UFW as the ban action?
If you use UFW as your firewall, configure Fail2Ban to insert ban rules through UFW. This is the simpler path, good for most setups.
Create a defaults file that sets UFW as the ban action. The filename zz-defaults.conf is intentional: on Ubuntu 24.04, the package ships defaults-debian.conf which sets banaction = nftables. Your file must sort alphabetically after it to override that default.
sudo tee /etc/fail2ban/jail.d/zz-defaults.conf > /dev/null << 'EOF'
[DEFAULT]
banaction = ufw
banaction_allports = ufw
backend = systemd
EOF
Restart and verify:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
When Fail2Ban bans an IP, it runs ufw insert 1 deny from <IP> to any. Check UFW rules after a ban occurs:
sudo ufw status numbered
You will see Fail2Ban's deny rules at the top of the list.
How do I configure Fail2Ban to use nftables instead of iptables?
For production servers running nftables directly (without UFW), use the nftables-multiport ban action. Fail2Ban creates its own nftables table and manages ban rules there without interfering with your existing ruleset.
Create the defaults file. On Ubuntu 24.04, defaults-debian.conf already sets banaction = nftables, so this step is technically optional. But it is good practice to be explicit. On Debian 12, this file is required.
sudo tee /etc/fail2ban/jail.d/zz-defaults.conf > /dev/null << 'EOF'
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables[type=allports]
chain = input
backend = systemd
EOF
Restart and verify:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
To confirm the nftables integration, list the Fail2Ban table:
sudo nft list tables
A table named inet f2b-table appears in the output. After a ban occurs, inspect its contents:
sudo nft list table inet f2b-table
UFW vs nftables ban action comparison
| Aspect | UFW (banaction = ufw) |
nftables (banaction = nftables-multiport) |
|---|---|---|
| Ban command | ufw insert 1 deny from <IP> |
Adds IP to nftables set |
| Where rules appear | ufw status numbered |
nft list table inet f2b-table |
| IPv6 support | Yes (UFW handles it) | Yes (inet family) |
| Persistence | UFW rules persist across reboots | Fail2Ban re-applies on start |
| Performance | Linear rule matching | Set-based lookup (faster at scale) |
| Best for | Simple setups, single services | Production, many jails, high ban counts |
Choose one approach. Do not mix UFW and nftables ban actions on the same server.
How do I whitelist my IP address in Fail2Ban?
Add your IP to ignoreip to prevent locking yourself out. This is important. If you mistype your password three times, Fail2Ban will ban your own IP. You will lose SSH access.
Add your IP to the defaults:
sudo tee /etc/fail2ban/jail.d/01-whitelist.conf > /dev/null << 'EOF'
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 YOUR_IP_HERE
EOF
Replace YOUR_IP_HERE with your actual public IP. You can find it by running curl -4 ifconfig.me from your local machine. Add multiple IPs separated by spaces.
Restart and verify the whitelist is loaded:
sudo systemctl restart fail2ban
sudo fail2ban-client get sshd ignoreip
The output lists all whitelisted IPs and ranges. Confirm your IP appears in the list.
Warning: If you connect from a dynamic IP, whitelisting it gives limited protection. Consider keeping a secondary access method (console access from your hosting provider's panel) as a backup in case you get banned.
How do I protect Nginx with Fail2Ban custom jails?
Fail2Ban ships with several Nginx filters. The most useful ones are nginx-http-auth for HTTP Basic Authentication brute-force and nginx-botsearch for scanners probing paths that do not exist. You can also write custom filters for application-specific patterns.
nginx-http-auth jail
This jail bans IPs that repeatedly fail HTTP Basic Authentication. It reads from the Nginx error log.
sudo tee /etc/fail2ban/jail.d/nginx-http-auth.conf > /dev/null << 'EOF'
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
findtime = 600
bantime = 3600
EOF
nginx-botsearch jail
This jail catches bots scanning for vulnerable paths (/wp-admin, /phpmyadmin, /.env). It reads the Nginx access log for 404 responses.
sudo tee /etc/fail2ban/jail.d/nginx-botsearch.conf > /dev/null << 'EOF'
[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 600
bantime = 7200
EOF
Custom filter: ban excessive 4xx errors
For applications behind Nginx, you may want to ban IPs generating too many 4xx errors. This catches credential stuffing, API abuse, and path enumeration. The built-in filters do not cover this, so create a custom filter.
Create the filter file:
sudo tee /etc/fail2ban/filter.d/nginx-4xx.conf > /dev/null << 'EOF'
[Definition]
failregex = ^<HOST> - \S+ \[.*\] "[A-Z]+ .+" (400|401|403|404|405) \d+
ignoreregex = ^<HOST> - \S+ \[.*\] "[A-Z]+ /favicon\.ico
^<HOST> - \S+ \[.*\] "[A-Z]+ /robots\.txt
EOF
The failregex matches Nginx access log lines where the response is a 4xx status code. The ignoreregex excludes common false positives like favicon.ico and robots.txt requests.
Create the jail:
sudo tee /etc/fail2ban/jail.d/nginx-4xx.conf > /dev/null << 'EOF'
[nginx-4xx]
enabled = true
port = http,https
filter = nginx-4xx
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 600
bantime = 3600
EOF
The higher maxretry of 20 avoids banning legitimate users who hit a few 404s during normal browsing.
Restart Fail2Ban and check all jails are loaded:
sudo systemctl restart fail2ban
sudo fail2ban-client status
Expected output shows all active jails:
Status
|- Number of jail: 4
`- Jail list: nginx-4xx, nginx-botsearch, nginx-http-auth, sshd
For more on protecting Nginx at the application layer, see .
How do I test Fail2Ban filters with fail2ban-regex?
Before enabling a jail, test its filter against real logs. The fail2ban-regex tool runs a filter against a log file and reports matches. This prevents deploying a filter that either matches nothing (useless) or matches everything (bans legitimate users).
Test the custom nginx-4xx filter:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-4xx.conf
Sample output:
Running tests
=============
Use failregex filter file : nginx-4xx, basedir: /etc/fail2ban
Use log file : /var/log/nginx/access.log
Use encoding : utf-8
Results
=======
Failregex: 42 total
|- #) [# of hits] regular expression
| 1) [42] ^<HOST> - \S+ \[.*\] "[A-Z]+ .+" (400|401|403|404|405) \d+
`-
Ignoreregex: 3 total
Date template hits:
...
Lines: 1842 lines, 0 ignored, 42 matched, 1800 missed
In this example, 42 lines matched out of 1842. That is a reasonable ratio. If the filter matched 90% of lines, the regex is too broad. If it matched 0 lines, either the regex is wrong or the log has no 4xx errors yet.
To see which lines matched:
sudo fail2ban-regex --print-all-matched /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-4xx.conf
To see which lines were missed (useful for tuning):
sudo fail2ban-regex --print-all-missed /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-4xx.conf
Test the built-in sshd filter against the systemd journal:
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
How do I set up the recidive jail for repeat offenders?
The recidive jail watches Fail2Ban's own log. When an IP gets banned multiple times across any jail, the recidive jail applies a longer ban. Think of it as escalation: first offense gets 1 hour, repeat offenders get 1 week.
On a busy production server, the recidive jail is what actually keeps persistent attackers out.
sudo tee /etc/fail2ban/jail.d/recidive.conf > /dev/null << 'EOF'
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
maxretry = 3
findtime = 86400
bantime = 604800
EOF
| Parameter | Value | Meaning |
|---|---|---|
logpath |
/var/log/fail2ban.log |
Fallback log path. With backend = systemd, Fail2Ban reads its own journal entries instead (the recidive filter has a built-in journalmatch directive) |
maxretry |
3 |
Triggers after 3 bans from other jails |
findtime |
86400 |
Looks back 24 hours (86400 seconds) |
bantime |
604800 |
Bans for 1 week (604800 seconds) |
banaction |
%(banaction_allports)s |
Blocks all ports, not just the one that triggered the ban |
The recidive filter ships with Fail2Ban at /etc/fail2ban/filter.d/recidive.conf. You do not need to create it.
Restart and verify:
sudo systemctl restart fail2ban
sudo fail2ban-client status recidive
How do I monitor bans and unban an IP address?
Fail2Ban provides fail2ban-client for all monitoring and management tasks. These commands work regardless of your ban action backend.
Check overall status
sudo fail2ban-client status
Lists all active jails and their ban counts.
Check a specific jail
sudo fail2ban-client status sshd
Shows currently banned IPs, total bans, and current failures.
Manually ban an IP
sudo fail2ban-client set sshd banip 203.0.113.50
Unban an IP
sudo fail2ban-client set sshd unbanip 203.0.113.50
If the IP was banned by the recidive jail, unban it there too:
sudo fail2ban-client set recidive unbanip 203.0.113.50
Check which ban action a jail uses
sudo fail2ban-client get sshd actions
Reload configuration without restarting
sudo fail2ban-client reload
Read the Fail2Ban log
journalctl -u fail2ban -f
This follows the Fail2Ban service log in real time. You will see ban and unban events as they happen.
The Fail2Ban application log with ban details:
sudo tail -f /var/log/fail2ban.log
fail2ban-client command reference
| Command | Purpose |
|---|---|
fail2ban-client status |
List all jails |
fail2ban-client status <jail> |
Show jail details and banned IPs |
fail2ban-client set <jail> banip <IP> |
Manually ban an IP |
fail2ban-client set <jail> unbanip <IP> |
Unban an IP |
fail2ban-client reload |
Reload all configuration |
fail2ban-client reload <jail> |
Reload a single jail |
fail2ban-client get <jail> bantime |
Show ban duration |
fail2ban-client get <jail> findtime |
Show failure window |
fail2ban-client get <jail> maxretry |
Show retry threshold |
fail2ban-client get <jail> ignoreip |
Show whitelisted IPs |
fail2ban-client get <jail> actions |
Show ban action in use |
End-to-end verification: trigger a ban and confirm it works
Do not assume your configuration works. Test it. Trigger a real ban and verify the entire chain: log detection, ban action, firewall rule, and unban.
Step 1: From a different machine (not your whitelisted IP), attempt SSH logins with a wrong password. After 3 failures (matching maxretry), the connection should be refused.
If you do not have a second machine, use fail2ban-client to simulate a ban:
sudo fail2ban-client set sshd banip 198.51.100.25
Step 2: Verify the ban is recorded:
sudo fail2ban-client status sshd
The banned IP should appear in Banned IP list.
Step 3: Verify the firewall rule exists.
For UFW:
sudo ufw status numbered | grep 198.51.100.25
For nftables:
sudo nft list table inet f2b-table
The IP should appear in a set within the table.
Step 4: Unban and verify removal:
sudo fail2ban-client set sshd unbanip 198.51.100.25
sudo fail2ban-client status sshd
The IP should no longer appear in the banned list. Check the firewall again to confirm the rule was removed.
Fail2Ban configuration parameter reference
| Parameter | Default | Recommended | Description |
|---|---|---|---|
bantime |
10m |
1h or 3600 |
Duration of the ban |
findtime |
10m |
10m or 600 |
Window for counting failures |
maxretry |
5 |
3 for SSH, 5-20 for web |
Failures before banning |
ignoreip |
127.0.0.1/8 ::1 |
Add your IP | IPs that are never banned |
banaction |
nftables (Ubuntu 24.04), iptables-multiport (upstream) |
ufw or nftables-multiport |
Firewall command to execute |
backend |
auto |
systemd |
Log source (systemd journal on modern distros) |
destemail |
root@localhost |
Your email | Alert recipient |
action |
%(action_)s |
%(action_mwl)s for email alerts |
What happens on ban |
Something went wrong?
Fail2Ban will not start: Check for syntax errors in your jail files. A missing bracket or invalid value will prevent startup.
sudo fail2ban-client -t
This tests the configuration without starting the service. Fix any errors it reports.
Jail shows 0 matches but attacks are happening: The backend is probably wrong. On Ubuntu 24.04 and Debian 12, use backend = systemd. If you use backend = auto and the system has no /var/log/auth.log, Fail2Ban reads nothing.
Banned IP can still connect: The ban action is not matching your firewall. If you use UFW, set banaction = ufw. If you use nftables directly, set banaction = nftables-multiport. Check that the firewall rule actually exists after a ban.
You banned yourself: Access your server through the hosting provider's web console (VNC/KVM). From there:
sudo fail2ban-client set sshd unbanip YOUR_IP
Or stop Fail2Ban entirely:
sudo systemctl stop fail2ban
Then fix your ignoreip configuration and restart.
Fail2Ban uses too much memory: On servers with large log files, Fail2Ban can consume memory. Set dbpurgeage to limit the database size:
sudo tee /etc/fail2ban/jail.d/99-performance.conf > /dev/null << 'EOF'
[DEFAULT]
dbpurgeage = 7d
EOF
Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.