How to Install and Configure Fail2Ban on Ubuntu 24.04
Within minutes of a new VPS coming online, automated scanners will start hammering port 22 with username and password guesses. If you tail /var/log/auth.log on a fresh server, you will typically see hundreds of failed SSH login attempts per hour from IPs scattered across the world. Fail2Ban is the classic, battle-tested answer to that noise. This guide walks you through installing, configuring, and extending Fail2Ban on Ubuntu 24.04 LTS, from your first apt install to a hardened setup that protects SSH, nginx, WordPress, Postfix, Dovecot, and Fail2Ban itself against repeat offenders.
Running mail, WordPress, or anything exposed to the public internet? You need Fail2Ban on the box today. A CloudCore Starter VPS is a solid home for your first hardened Ubuntu server.
Table of Contents
What is Fail2Ban?
Fail2Ban is a log-scanning intrusion prevention framework written in Python. It watches log files for patterns that indicate abuse -- repeated failed SSH logins, nginx 401 responses, WordPress wp-login.php brute force, SMTP auth failures -- and bans the offending IP address by inserting a firewall rule. Bans are automatic, time-limited, and configurable per service.
The architecture is small and focused. A daemon (fail2ban-server) loads jails, each jail pairs a filter (a regex that extracts IP addresses from matching log lines) with an action (what to do when a threshold is crossed). Filters live in /etc/fail2ban/filter.d/, actions in /etc/fail2ban/action.d/, and jails are declared in /etc/fail2ban/jail.conf or, much more safely, in your own /etc/fail2ban/jail.local override.
Fail2Ban ships with filters for dozens of common services out of the box: OpenSSH, Postfix, Dovecot, Exim, ProFTPD, vsftpd, Apache, nginx, Roundcube, Asterisk, CouchDB, named, and many more. The action side is just as flexible -- block via iptables, nftables, ufw, ipset, Cloudflare's API, AWS security groups, or a simple /etc/hosts.deny entry. You can also chain actions, so a single ban can simultaneously block at the kernel firewall, report the IP to AbuseIPDB, and send you an email.
Where it really pays off is the recidive jail -- a meta-jail that scans Fail2Ban's own log and bans IPs that keep getting banned by other jails. An attacker who trips the SSH jail three times in a day earns a one-week ban covering every protocol on the server. That single jail stops almost all long-running brute force campaigns cold.
Why Every Server Needs Fail2Ban
If you have ever watched a fresh VPS's auth.log, you already know the answer: the public IPv4 space is under constant scan. A new VPS on a major provider receives its first SSH connection attempt within roughly 90 seconds of booting. Within the first hour, expect 200-500 login attempts against root, admin, ubuntu, postgres, and a rotating cast of 50 common usernames. This is baseline background noise, not a targeted attack -- and it never stops.
Without a rate limiter, three things go wrong:
- Log noise drowns out real events. Genuine failed logins from your own team vanish into a wall of garbage.
- CPU and I/O are wasted rejecting thousands of connections per hour. On a small VPS this is measurable.
- Any weak password eventually loses. Most brute force campaigns are slow and patient. Given an unlimited number of tries, a dictionary of 10 million passwords, and enough time, attackers win against an 8-character password.
Fail2Ban is not a replacement for SSH key auth, strong passwords, or a firewall -- it is a complement. Run all of them together.
Prerequisites
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- Basic familiarity with systemd and journalctl
- UFW, iptables, or nftables already active (Fail2Ban uses whichever is present)
- (Optional) A working mail relay (or Postfix in local-only mode) if you want email notifications
Recommended Plan: CloudCore Starter>
Fail2Ban is lightweight -- it runs happily on the smallest VPS. For most personal and small business workloads we recommend the CloudCore Starter plan. It gives you enough headroom for Fail2Ban plus SSH, nginx, Postfix, and a typical WordPress or Node.js site.
Connect to your server to get started:
ssh root@your-server-ipStep 1: Update System Packages
Before installing anything, refresh your package index and install outstanding security updates:
sudo apt update && sudo apt upgrade -yIf the kernel was updated, reboot before continuing:
sudo rebootStep 2: Install Fail2Ban
Fail2Ban is in Ubuntu's main repositories. Install it with:
sudo apt install -y fail2banExpected output (abbreviated):
The following NEW packages will be installed:
fail2ban python3-pyinotify python3-systemd whois
...
Setting up fail2ban (1.0.2-3ubuntu0.1) ...
Created symlink /etc/systemd/system/multi-user.target.wants/fail2ban.service -> /lib/systemd/system/fail2ban.service.The install pulls in python3-systemd (so Fail2Ban can read the journal directly) and whois (used by one of the bundled notification actions). The fail2ban.service systemd unit is enabled and started automatically.
Verify the installed version:
fail2ban-client --versionExpected output:
Fail2Ban v1.0.2Step 3: Enable and Start the Service
The package enables itself on install, but confirm:
sudo systemctl enable --now fail2ban
sudo systemctl status fail2banExpected output:
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-04-16 10:00:00 UTC; 10s ago
Docs: man:fail2ban(1)
Main PID: 1234 (fail2ban-server)
Tasks: 5 (limit: 4915)
Memory: 12.3M
CPU: 180msCheck that Fail2Ban sees at least the default sshd jail:
sudo fail2ban-client statusExpected output:
Status |- Number of jail: 1- Jail list: sshd</code></pre></div>
Fail2Ban ships with thesshdjail enabled by default on Ubuntu, so you already have basic SSH protection from this point forward. Everything else in this guide builds on that baseline.
Step 4: Create jail.local
Rule one of Fail2Ban administration: do not edit/etc/fail2ban/jail.conf.That file is shipped and overwritten by the Debian/Ubuntu package on every upgrade. Instead, create ajail.localthat overrides it. Both files are read, and settings injail.localwin.
Create the file:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo nano /etc/fail2ban/jail.local</code></pre></div>
Start with a[DEFAULT]block that sets sensible global policy for every jail:
<div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[DEFAULT]
How long a ban lasts (1 hour)bantime = 1hThe window within which maxretry is counted (10 minutes)
findtime = 10mFailures allowed within findtime before a ban
maxretry = 5Read logs from systemd journal (preferred on Ubuntu 24.04)
backend = systemdDefault ban action -- use ufw if UFW is active, otherwise iptables-multiport
banaction = ufw banaction_allports = ufwTrust these addresses and never ban them
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16Destination email for notifications (only used if you enable mail actions)
destemail = [email protected] sender = [email protected] mta = sendmailDefault action: ban only (no email). Set to action_mwl for ban + mail with whois + log.
action = %(action_)s</code></pre></div>A few notes on what you just wrote:
backend = systemdtells Fail2Ban to read events from the journal instead of tailing flat log files. On Ubuntu 24.04, SSH, nginx, Postfix, and Dovecot all log through systemd, which makes this backend more reliable thanautoorpolling. It also handles log rotation without any restart.banaction = ufwassumes you are using UFW as your firewall front-end (the default on many Ubuntu installs). If you are using raw iptables, change bothbanactionandbanaction_allportstoiptables-multiport. If you are on nftables, usenftables-multiport.ignoreipis a space-separated list of CIDR ranges that will never be banned. Add your home IP, your VPN's egress IP, and your office range before you start testing jails, otherwise a typo in your own password could lock you out.Save and close. Do not reload Fail2Ban yet -- we will add the individual jails first.Step 5: Configure the SSH Jail
Append the SSH jail tojail.local:
<div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[sshd] enabled = true port = ssh filter = sshd backend = systemd maxretry = 5 findtime = 10m bantime = 1h</code></pre></div>
On Ubuntu 24.04 the OpenSSH server logs to the systemd journal, not to/var/log/auth.log. Thebackend = systemdsetting above tells Fail2Ban to read journald, so you do not need alogpathentry. If you prefer to tail the legacy file (for example, because you have forwarded it elsewhere with rsyslog), swap the backend and addlogpath = /var/log/auth.loginstead.
If you have moved SSH off port 22, updateportaccordingly -- for exampleport = 2222-- so that the ban action knows which port to block.
Reload Fail2Ban:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban</code></pre></div>
Check the jail came up:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo fail2ban-client status sshd</code></pre></div>
Expected output:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">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:
You are now protected against SSH brute force.
Step 6: Add Nginx Jails
If you run nginx, three built-in filters are worth enabling straight away: nginx-http-auth (brute force against HTTP basic auth), nginx-limit-req (clients breaching limit_req zones), and nginx-botsearch (scanners probing for /wp-admin, /.env, /phpmyadmin, and similar).
Append to jail.local:
[nginx-http-auth] enabled = true filter = nginx-http-auth port = http,https logpath = /var/log/nginx/error.log maxretry = 5[nginx-limit-req] enabled = true filter = nginx-limit-req port = http,https logpath = /var/log/nginx/error.log maxretry = 10 findtime = 5m bantime = 30m
[nginx-botsearch] enabled = true filter = nginx-botsearch port = http,https logpath = /var/log/nginx/access.log maxretry = 2 findtime = 1d bantime = 1w
Notes:
- The first two read
error.logbecause auth failures andlimit_reqrejections appear there.nginx-botsearchreadsaccess.logbecause it pattern-matches on suspicious request URIs in successful-parse entries. nginx-botsearchis aggressive by design -- two hits to/.envor/wp-login.phpon a non-WordPress site is almost always malicious. We setbantime = 1wto hold offenders for a full week.- For
nginx-limit-reqto do anything useful, you need alimit_req_zone/limit_reqdirective in your nginx config that produces "limiting requests" log lines when breached.
sudo systemctl reload fail2ban
sudo fail2ban-client statusYou should now see all four jails listed.
Step 7: Protect WordPress with a Custom Filter
Fail2Ban does not ship with a WordPress filter, but writing one takes two minutes. It targets the classic wp-login.php POST brute force pattern.
Create the filter:
sudo nano /etc/fail2ban/filter.d/wordpress.confPaste:
[Definition]
failregex = ^<HOST> . "POST /wp-login\.php." (200|401|403) .*$
^<HOST> . "POST /xmlrpc\.php." (200|401|403) .*$
ignoreregex =The <HOST> macro captures the client IP. The regex matches any POST to wp-login.php or xmlrpc.php. Because a real human login is also a POST that returns 200, we rely on frequency (maxretry) rather than trying to distinguish success from failure in the log line -- 5 logins in 10 minutes is almost never a legitimate user.
Add the jail to jail.local:
[wordpress]
enabled = true
filter = wordpress
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 10m
bantime = 2hBefore reloading, test the filter against your live access log:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress.confExpected output (numbers will vary):
Running tests =============Use failregex filter file : wordpress, basedir: /etc/fail2ban Use log file : /var/log/nginx/access.log Use encoding : UTF-8
Results =======
Failregex: 42 total |- #) [# of hits] regular expression | 1) [28] ^<HOST> . "POST /wp-login\.php." (200|401|403) .*$ | 2) [14] ^<HOST> . "POST /xmlrpc\.php." (200|401|403) .*$
-</code></pre></div>
If you see0 totalhits but you know there should be matches, your log format differs from the default and you will need to adjust the regex. Reload once the test passes:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban</code></pre></div>
Step 8: Postfix and Dovecot Jails
If this server handles mail, enable the Postfix and Dovecot jails to stop SMTP/IMAP brute force:
<div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[postfix] enabled = true filter = postfix port = smtp,465,submission backend = systemd maxretry = 5 bantime = 1h
[postfix-sasl] enabled = true filter = postfix-sasl port = smtp,465,submission,imap,imaps,pop3,pop3s backend = systemd maxretry = 3 bantime = 2h
[dovecot] enabled = true filter = dovecot port = pop3,pop3s,imap,imaps,submission,465,sieve backend = systemd maxretry = 5 bantime = 1h</code></pre></div>
Thepostfix-sasljail is narrower and stricter than the plainpostfixjail: it triggers only on authenticated submission failures, which are a near-certain sign of attempted relay abuse. Three strikes and you're out.
Reload:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban sudo fail2ban-client status postfix-sasl</code></pre></div>
Step 9: Enable Recidive for Repeat Offenders
The recidive jail is the single most valuable jail aftersshd. It watches Fail2Ban's own log for ban events and bans any IP that trips any jail multiple times.
Append:
<div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[recidive] enabled = true filter = recidive logpath = /var/log/fail2ban.log action = %(banaction_allports)s[name=%(__name__)s] bantime = 1w findtime = 1d maxretry = 3</code></pre></div>
This says: if an IP gets banned by any other jail 3 times within 24 hours, ban it across all ports for a full week. That turns one bad SSH session into a blanket ban covering SSH, HTTP, HTTPS, SMTP, and IMAP.
Note that recidive uses a file logpath (/var/log/fail2ban.log), not systemd. Fail2Ban writes its own action log to that file by default on Ubuntu, so no configuration change is needed.
Reload:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban sudo fail2ban-client status recidive</code></pre></div>
Step 10: Write and Test a Custom Filter
At some point you will need a filter for software that Fail2Ban doesn't know about -- a custom web app, a game server, an API with a custom auth log. The pattern is always the same.
Assume you run a Node.js app that writes failed login lines like this to/var/log/myapp/app.log:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">2026-04-16T10:15:22Z WARN login_failed ip=203.0.113.47 user=admin reason=bad_password</code></pre></div>
Create a filter:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo nano /etc/fail2ban/filter.d/myapp.conf</code></pre></div>
Paste:
<div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[Definition] failregex = ^. WARN login_failed ip=<HOST> .reason=bad_password.*$ ignoreregex =</code></pre></div>
Two important pieces:
failregexis the pattern that identifies a failure. Themacro is required -- it is what Fail2Ban uses to extract the IP. If your regex has no, Fail2Ban silently matches nothing.ignoreregexlets you exclude specific lines that would otherwise match. Useful for suppressing legitimate cases (for example, health-check IPs or known monitoring probes).Test before wiring it up:<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo fail2ban-regex /var/log/myapp/app.log /etc/fail2ban/filter.d/myapp.conf</code></pre></div>
You want to see a non-zeroFailregexhit count and0underIgnoreregex. You can also test with canned input:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo fail2ban-regex \ '2026-04-16T10:15:22Z WARN login_failed ip=203.0.113.47 user=admin reason=bad_password' \ /etc/fail2ban/filter.d/myapp.conf</code></pre></div>
Once the filter matches, add a jail tojail.local:
<div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[myapp] enabled = true filter = myapp port = http,https logpath = /var/log/myapp/app.log maxretry = 5</code></pre></div>
And reload.
Using fail2ban-clientfail2ban-client
is the day-to-day operator tool. The commands you will actually use:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash"># Overall status -- which jails are running sudo fail2ban-client status
Status of a specific jail, including currently banned IPssudo fail2ban-client status sshdManually ban an IP in a jail
sudo fail2ban-client set sshd banip 198.51.100.22Manually unban an IP in a jail
sudo fail2ban-client set sshd unbanip 198.51.100.22Unban an IP from every jail that currently holds it
sudo fail2ban-client unban 198.51.100.22Unban every IP across every jail (emergency use)
sudo fail2ban-client unban --allReload after editing jail.local or a filter
sudo fail2ban-client reloadReload a single jail (faster, no full restart)
sudo fail2ban-client reload sshdShow the current runtime value of a jail setting
sudo fail2ban-client get sshd bantimeChange a runtime setting without editing config (lost on restart)
sudo fail2ban-client set sshd bantime 7200</code></pre></div>
Output ofstatus sshdon a server that has seen real traffic:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Status for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 1847 |- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd- Actions |- Currently banned: 4 |- Total banned: 219- Banned IP list: 45.148.10.12 141.98.11.22 193.32.162.56 218.92.0.14
Email Notifications
To get an email every time an IP is banned, change the jail action from %(action_)s (ban only) to %(action_mwl)s (ban + mail with whois + log snippet).
In jail.local, either in [DEFAULT] for everything or per-jail:
[sshd]
enabled = true
action = %(action_mwl)s
destemail = [email protected]
sender = [email protected]The mwl action uses the sendmail-whois-lines action under the hood: it runs whois against the banned IP, pulls the matching lines out of your log, and mails the full bundle. Emails look like this:
Subject: [Fail2Ban] sshd: banned 203.0.113.47 from ...Hi,
The IP 203.0.113.47 has just been banned by Fail2Ban after 5 attempts against sshd.
Here is more information about 203.0.113.47 (from whois ...): inetnum: 203.0.113.0 - 203.0.113.255 ...
Lines containing failures of 203.0.113.47: Apr 16 10:15:02 srv sshd[1847]: Failed password for root from 203.0.113.47 ...
action_mw (without the l) skips the log lines -- use it if emails are too noisy.
For this to work, sendmail (or a drop-in like msmtp, postfix in null-client mode, or ssmtp) must be installed and able to relay mail. On a fresh VPS, sudo apt install -y postfix and selecting "Internet Site" is the quickest path.
Whitelisting and ignoreip
Never ban yourself. The ignoreip directive accepts a space-separated list of:
- IPv4 or IPv6 addresses
- CIDR ranges (
10.0.0.0/8,2001:db8::/32) - DNS names -- Fail2Ban resolves them at start time
[DEFAULT] block:ignoreip = 127.0.0.1/8 ::1 203.0.113.42 192.168.0.0/16 home.example.comKeep in mind:
- DNS-based whitelisting is resolved once at startup (and on reload). A dynamic hostname that changes IP will stop working until you reload.
ignoreipapplies to all jails. You can override it per-jail if, for example, you want monitoring IPs to be ignored bysshdbut not bynginx-limit-req.- Use
ignoreself = true(the default) to automatically exempt the server's own interfaces.
ignoreip before you reload, or your first mistyped password will lock you out.Edge Banning with Cloudflare and AbuseIPDB
Fail2Ban bans at your server's firewall, which is great but wastes a round trip -- the attacker still reaches your edge. If you run behind Cloudflare, push bans one layer further out.
Cloudflare API action
Ubuntu ships cloudflare.conf and cloudflare-token.conf under /etc/fail2ban/action.d/. The token-based one is current and preferred. Create an API token in your Cloudflare dashboard with Zone -> Firewall Services -> Edit permission scoped to the zone you want to protect.
Drop a credentials file:
sudo mkdir -p /etc/fail2ban/action.d
sudo tee /etc/fail2ban/action.d/cloudflare-token.local > /dev/null <<'EOF'
[Init]
cftoken = YOUR_CLOUDFLARE_API_TOKEN_HERE
EOF
sudo chmod 600 /etc/fail2ban/action.d/cloudflare-token.localThen chain the action on a web-facing jail:
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 1w
action = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s"]
cloudflare-token[cftoken="YOUR_CLOUDFLARE_API_TOKEN_HERE"]Now a ban lands both in your local firewall and Cloudflare's edge rules, so the attacker never reaches your origin.
AbuseIPDB reporting
AbuseIPDB crowdsources reputation data. Fail2Ban can report every ban back to the database, which helps everyone. Get an API key at abuseipdb.com and add the action:
[sshd]
action = %(action_)s
abuseipdb[abuseipdb_apikey="YOUR_KEY", abuseipdb_category="18,22"]Category 18 is "Brute-Force" and 22 is "SSH". See /etc/fail2ban/action.d/abuseipdb.conf for the full category list.
Persistent Bans and Incremental Ban Time
By default, bans evaporate when Fail2Ban restarts, because the ban list lives in memory. Fail2Ban 0.11+ (Ubuntu 24.04 ships 1.0.2) supports persistent bans via a SQLite database at /var/lib/fail2ban/fail2ban.sqlite3 -- it is enabled by default. When the daemon starts, any still-active bans are re-applied to the firewall.
What most people don't enable is incremental ban time, which makes each subsequent ban longer than the last. Add to [DEFAULT]:
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1w
bantime.rndtime = 10m
bantime.overalljails = trueWith these settings:
- First ban =
bantime(1h) - Second ban = 2h
- Third ban = 4h, then 8h, 16h, 32h, 64h, capped at
1w rndtimeadds a random 0-10 minute jitter so botnets can't sync around your unban scheduleoveralljails = truemeans a ban in any jail counts toward the increment, not just the same jail
IPv6 Notes
Ubuntu 24.04 supports IPv6 out of the box, and so does Fail2Ban. The bundled banactions (ufw, iptables-multiport, nftables-multiport) all handle v6 correctly when the underlying firewall is configured for v6 as well. Two small things to check:
- If you're on UFW, make sure
IPV6=yesis set in/etc/default/ufw(it is by default). - Your
ignoreiplist should include::1and any v6 ranges you trust. - The systemd journal backend picks up v6 addresses just like v4.
sudo fail2ban-client status sshdIf you have had real IPv6 brute force, you should see v6 addresses in Banned IP list.
A Jail for Fail2Ban Itself
The recidive jail you enabled in Step 9 already protects Fail2Ban itself: if an attacker keeps getting banned by any jail, recidive escalates them to a week-long all-ports ban. This is the community-standard way to harden Fail2Ban against patient attackers who rotate IPs slowly -- it turns "stay under maxretry and try again in an hour" into "get caught three times in a day and lose a week of access."
If you want stricter behaviour, lower recidive's maxretry to 2 and raise bantime to 4w.
Performance Tuning
Fail2Ban is efficient but not free. On large systems with GB-sized log files and hundreds of active jails, a few tweaks help.
Prefer the systemd backend
On Ubuntu 24.04, backend = systemd is dramatically faster than polling flat files. The journal indexes entries by unit, so Fail2Ban reads only lines that could possibly match. Polling a 2 GB access log on every scan cycle is much more expensive.
Rotate aggressively
If you must use a file backend, make sure logrotate is running daily and keeps logs trim. A 500 MB access.log will slow the access.log-based nginx jails' cold-start scan considerably.
Use ipset or nftables for large ban lists
iptables-multiport inserts one rule per banned IP. Once you have more than a few hundred active bans, packet filtering slows down. Switch to iptables-ipset-proto6 (or use nftables' native set support) so bans live in a kernel hash set that matches in O(1). On Ubuntu 24.04, nftables is the modern default:
banaction = nftables-multiport
banaction_allports = nftables-allportsReduce findtime on noisy jails
If nginx-limit-req fires constantly, reduce its findtime from 10m to 1m. That keeps the internal failure queue small and bans snappier.
Disable jails you don't need
Every enabled jail consumes CPU per incoming log event. If you don't run Postfix, don't enable postfix. If you don't run Dovecot, don't enable dovecot. Less is more.
Logging Configuration
Fail2Ban's own log is at /var/log/fail2ban.log by default. Adjust verbosity in /etc/fail2ban/fail2ban.local (create it -- again, do not edit fail2ban.conf):
[Definition]
loglevel = INFO
logtarget = /var/log/fail2ban.logLoglevels in order of verbosity: CRITICAL, ERROR, WARNING, NOTICE, INFO (default), DEBUG, TRACEDEBUG, HEAVYDEBUG. Bump to DEBUG temporarily when troubleshooting a regex, then put it back -- DEBUG fills disk fast on a busy server.
Tail the log to watch bans in real time:
sudo tail -f /var/log/fail2ban.logTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Jail starts but never bans anything | failregex doesn't match actual log format | Run sudo fail2ban-regex <logfile> <filter.conf>. If Failregex: 0 total, your regex is wrong. Compare a real log line against the pattern character by character. |
systemctl status fail2ban shows "failed" after reload | Syntax error in jail.local or a filter | sudo fail2ban-client -d prints the full merged config and errors on the first problem. journalctl -u fail2ban -n 50 shows the exception. |
fail2ban-client set sshd banip X.X.X.X returns success but no firewall rule appears | UFW disabled, or banaction mismatched with actual firewall | Check sudo ufw status. If inactive, either enable UFW or change banaction to iptables-multiport. Verify with sudo iptables -L -n \</td><td>grep X.X.X.X. |
| Whitelist doesn't work | Typo in ignoreip, or hostname hasn't been re-resolved | sudo fail2ban-client get DEFAULT ignoreip shows the active list. Hostnames resolve at startup only -- sudo systemctl restart fail2ban after DNS changes. |
| CPU spikes to 100% | Regex catastrophic backtracking, or polling huge files | Check with sudo fail2ban-regex --verbose. Switch to backend = systemd. Rewrite greedy . patterns to non-greedy .? or anchored alternatives. |
| Bans don't persist across reboots | SQLite DB disabled or world-writable /var/lib/fail2ban was cleared | Check /etc/fail2ban/fail2ban.local for dbfile. Default is /var/lib/fail2ban/fail2ban.sqlite3. It should exist and be writable by root. |
IPv6 addresses show up as ? | Older Fail2Ban on a mismatched filter | Ubuntu 24.04 ships a recent enough build that this is not an issue. If you see it, update the filter in /etc/fail2ban/filter.d/. |
| recidive jail never bans anything despite SSH bans firing | logpath for recidive points at the wrong file | Confirm Fail2Ban is logging to /var/log/fail2ban.log (not journal). Check fail2ban.conf for logtarget. |
| You locked yourself out | Too aggressive, no ignoreip | From the provider's web console or KVM, sudo fail2ban-client unban <your-ip>, then add your IP to ignoreip before reloading. |
Essential Diagnostic Commands
# Real-time Fail2Ban log
sudo tail -f /var/log/fail2ban.logSystemd service log
sudo journalctl -u fail2ban -fDump the full merged configuration
sudo fail2ban-client -dTest a filter against a live log
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.confList every jail and its current counters
sudo fail2ban-client statusFAQ
Fail2Ban vs CrowdSec -- which should I use?
Both scan logs and ban abusers, but they solve slightly different problems. Fail2Ban is a classic, minimal, self-contained daemon: one server, one log, one ban list, no external dependencies. Every ban is local. CrowdSec is a newer design built around a shared reputation network: your local agent detects abusers and reports them, and in return you pull down a community blocklist of IPs that other CrowdSec users have seen attacking them. CrowdSec scales better to large fleets, but Fail2Ban is simpler, more scriptable, and has been battle-tested for two decades. For a single VPS or a small fleet, Fail2Ban is often the right choice. For a larger attack surface where you want network effects, add CrowdSec. Many operators run both: Fail2Ban on the edge for immediate reaction, CrowdSec for crowd-sourced reputation. See our CrowdSec installation guide for the complementary setup.
Fail2Ban vs a cloud WAF like Cloudflare -- do I need both?
Yes. A WAF stops attacks at the edge based on static rulesets (OWASP rules, rate limits per path, bot scoring), and it is excellent at that. Fail2Ban stops attacks at the origin based on your service's actual authentication logs. The WAF doesn't know whether your SSH logins are succeeding, and Fail2Ban doesn't know what the attacker's browser fingerprint looks like. They are complementary layers. The Cloudflare action integration described above connects them -- Fail2Ban detects at the origin and pushes bans back to the edge -- which gives you both signals.
How do I deal with false positives?
Three techniques, in order of preference: (1) whitelist the source in ignoreip, (2) add specific patterns to the filter's ignoreregex so they don't count as failures, (3) lower maxretry thresholds so genuine retries don't trigger bans. If you find yourself banning a single IP repeatedly for no reason, use sudo fail2ban-regex against a real log sample to see which regex is matching -- it is almost always a too-greedy failregex. The fix is to tighten the pattern, not to raise the threshold.
Can Fail2Ban do country-based (GeoIP) blocking?
Not natively. Fail2Ban bans individual IPs based on log events, not entire countries. For geo-blocking, use your firewall's native geoip module: ipset with MaxMind's GeoLite2 data, nftables' @geoip sets, or a CDN's country-level firewall rule. You can combine the two -- geoip-block entire regions you never serve, and let Fail2Ban handle the long tail inside allowed regions.
Does Fail2Ban support ipset?
Yes. The iptables-ipset-proto6 and iptables-ipset-proto6-allports banactions store banned IPs in an ipset hash rather than inserting one iptables rule per IP. This is essential for servers that accumulate thousands of bans -- kernel lookups stay O(1) regardless of list size. On Ubuntu 24.04 with nftables as the default backend, use nftables-multiport instead, which uses native nftables sets and is equally efficient.
How do I run Fail2Ban in a Docker container?
Two approaches. The easy one: run Fail2Ban on the host and let it watch logs from containers mounted in (via bind-mounts) or read from the journal (the journal contains all container logs if you use the journald log driver). The harder one: run Fail2Ban inside a container with --cap-add=NET_ADMIN and --network=host, reading mounted log volumes. Either works, but the host-based approach is almost always simpler and avoids the complication of a container trying to manage the host's firewall. If you run the whole app stack inside containers behind Traefik or nginx, have Traefik log access events in a format Fail2Ban understands and write a filter for it.
Does reload or restart drop active bans?
fail2ban-client reload preserves active bans (and the SQLite backing store). A full systemctl restart fail2ban also preserves them, because the database is read back on startup and the firewall rules are re-inserted. Prefer reload for config changes -- it is faster and avoids a small window where the service is down.
Next Steps
Now that Fail2Ban is protecting your server, build on the foundation:
- Layer CrowdSec on top -- Read our CrowdSec install guide to add crowd-sourced threat intelligence alongside your local Fail2Ban rules. The two together cover both known-bad IPs (CrowdSec) and your own service's live attackers (Fail2Ban).
- Move SSH behind a VPN -- The best rate limiter is "no public SSH at all." Set up WireGuard on Ubuntu 24.04 and bind SSH to the VPN interface only. Fail2Ban then guards everything else.
- Harden nginx -- Our nginx hardening guide covers TLS, security headers, rate limiting, and
limit_reqzones that feed thenginx-limit-reqjail. - Monitor bans over time -- Ship
/var/log/fail2ban.logto a central log store (Loki, ELK, or even a simple Grafana Cloud trial) and graph bans per day. You will spot attack campaigns as they ramp up. - Audit your filter regexes quarterly -- Log formats change as software updates. A filter that matched last year may silently match nothing today.
fail2ban-regexagainst a recent log sample takes 30 seconds and catches these regressions.
Need a server to run Fail2Ban on?>
Every CloudCore Starter VPS gives you a clean Ubuntu 24.04 LTS install with full root access, a real public IP, and enough headroom to run Fail2Ban, UFW, nginx, and a handful of services without breaking a sweat.>
- Ubuntu 24.04 LTS pre-installed
- Full root / sudo access
- Dedicated IPv4 + IPv6
- 99.9% uptime SLA
- Deploy in under 60 seconds>
Launch Your Hardened VPS and start banning bots before your first coffee break.