Install and Configure Fail2Ban on a Linux VPS

12 min read·Matthieu·UbuntuDebianUFWnftablesNginxSSHSecurityFail2Ban|

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.

Ready to try it yourself?

Protect your Linux VPS with Fail2Ban and more.

See VPS Plans