Every internet-facing Linux server is under constant attack. Within minutes of a fresh cloud instance getting a public IP, automated bots start hammering port 22 with stolen credentials, dictionary lists, and worm payloads. The data backs this up: in its Q4 2025 report on malware targeting Linux SSH servers, AhnLab’s Security Intelligence Center found that the self-propagating P2PInfect worm alone accounted for Over 20 million SSH brute-force attempts were recorded, with top IPs showing botnet signs, but no single malware accounts for 80.4% of the total, while Prometei represents 8.3% and XMRig represents 2.2% of a different metric [1][9].Prometei accounts for 8.3% and XMRig accounts for 2.2% of SSH brute-force activity, but these figures do not sum to 5.2% of 80.4% as implied, since over 20 million attempts were recorded with no single malware dominating 80.4% [1].4%) close behind. The 2025 Verizon Data Breach Investigations Report measured brute-force attacks against basic web applications nearly tripling year over year, climbing from roughly 20% to 60% of web-app incidents.
This fail2ban tutorial shows you how to shut that traffic down. Fail2ban is a free, open-source intrusion-prevention framework that watches your log files, recognises repeated authentication failures, and automatically inserts firewall rules to ban the offending IP addresses. It has protected production servers for over a decade, carries more than 18,000 stars on GitHub, and is still the fastest way to cut brute-force noise from your auth logs. Over the next 12 steps – about 30 minutes of hands-on work – you will install fail2ban, build a hardened jail.local, protect SSH and Nginx, write and test a custom filter, add a recidive jail for repeat offenders, and verify a real ban end to end on Ubuntu 24.04 LTS.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Fail2ban Still Matters for SSH Brute-Force Protection in 2026
Cloud security groups and host firewalls decide which ports are open. They cannot tell the difference between you typing the wrong SSH password once and a botnet trying 50,000 passwords an hour from the same address. That gap is exactly where fail2ban lives. It reads the same logs your firewall ignores, applies a “too many failures in too little time” rule, and converts the verdict into a temporary firewall ban. The attacker’s connections start timing out; your CPU, your auth logs, and your security team all get quieter.
The threat surface is not shrinking. The Shadowserver Foundation’s honeypot brute-force reporting tracks millions of credential-guessing events across its global sensor network every day, and AhnLab’s quarterly honeypot data shows the attacker toolkit shifting from manual logins to self-replicating worms that pivot the moment one weak password falls. P2PInfect’s jump from SSH brute-force activity reached 80.4% in Q1 2025, but Akira ransomware activity declined from 226 in Q4 2025 to 176 in Q1 2026 (a 22% decrease), not 80.4% [4].Akira ransomware activity declined from 226 in Q4 2025 to 176 in Q1 2026, and while a worm landing on one box can weaponize it to attack thousands more, this behavior is not quantified at 4% [4]. A single banned IP today can prevent a lateral-movement incident tomorrow.
Fail2ban is also cheap insurance. It runs in a few megabytes of RAM, ships in every major distribution’s repository, and needs no agent, no cloud account, and no licence. For teams that have watched headline breaches unfold – like the ShinyHunters campaign that hit 100+ organisations through an Oracle zero-day – fail2ban will not stop a software vulnerability, but it removes the single most common attacker entry point: a guessable password on an exposed service. Pair it with key-based SSH, a real firewall, and patching, and you have closed the door that the vast majority of opportunistic bots are knocking on.
How Fail2ban Works: Jails, Filters, Actions, and Backends
Before you touch a config file, it pays to understand the four moving parts fail2ban bolts together. Everything you configure in this fail2ban tutorial is one of these components. A filter is a set of regular expressions that recognise a failure line in a log. A jail ties a filter to a log source, a threshold (how many failures in what window), and an action. An action is the command fail2ban runs when the threshold is crossed – almost always “add a firewall rule banning this host.” A backend is how fail2ban reads the logs in the first place: by polling files, watching them with inotify, or reading the systemd journal.
| Component | What it does | Where it lives | Example |
|---|---|---|---|
| Filter | Regex that matches a failed-login line and extracts the attacker IP via the <HOST> token | /etc/fail2ban/filter.d/ | sshd.conf, nginx-http-auth.conf |
| Jail | Binds a filter to a log path, sets maxretry/findtime/bantime, and names the action | /etc/fail2ban/jail.local | [sshd], [nginx-botsearch] |
| Action | The command run on a match – usually a firewall ban, optionally an email | /etc/fail2ban/action.d/ | nftables-multiport, action_mwl |
| Backend | How logs are read: systemd (journal), pyinotify, polling, or auto | Set per jail or in [DEFAULT] | backend = systemd |
| Database | SQLite store of active bans, used to restore them after a restart | /var/lib/fail2ban/fail2ban.sqlite3 | Survives reboots |
The flow is simple: the backend feeds new log lines to the filter, the filter pulls out the IP of any line that matches its failregex, the jail counts how many times that IP has failed inside the findtime window, and when the count hits maxretry the action fires and bans the IP for bantime. When the ban expires, the action’s unban command removes the firewall rule. That loop, repeated across every jail you enable, is the whole product. The rest of this guide is about wiring it up safely so you protect your services without locking yourself out.
Prerequisites and Versions
This walkthrough targets a modern systemd-based Linux server. Everything applies with minor path changes to Debian 12, Rocky/AlmaLinux 9, and Fedora, all of which now track the actively developed 1.1.0 branch – Fedora’s packaging even picked up a Dovecot 2.4 jail patch in the 1.1.0-15.fc43 update shipped in December 2025, and Fedora Release Engineering’s own changelog shows the package rebuilt four more times between January and July 2026 as the branch kept receiving upkeep. Here is what you need before Step 1:
- A Linux server with root or sudo – Ubuntu 24.04 LTS (“Noble Numbat”) is used in the examples. A $5 VPS or a local VM is fine.
- Fail2ban 1.1.0.x or newer – package trackers show the 1.1.0 branch rolling out steadily across distributions: Debian moved fail2ban 1.1.0-8 into testing in May 2025, the same month Launchpad’s Ubuntu changelog logged its own maintainer update entry for the package, with 1.1.0-9 reaching unstable by December 2025 (the Ubuntu archive lists that build’s Debian source package at 411KB), Gentoo stabilized 1.1.0-r6 in October 2025, and GNU Guix has packaged fail2ban 1.1.0 as its listed version since September 2025 – and the branch has kept moving since: Debian accepted fail2ban 1.1.1-1 into unstable in September 2026 and migrated it to testing that same month, while Kali Linux pulled 1.1.0-10 into its repositories in May 2026 before importing 1.1.1-1 into kali-rolling in September 2026. Confirm yours with
fail2ban-client versionafter install. - Python 3 – fail2ban is written in Python and bundles with every modern distribution’s Python 3 runtime. You do not install it separately.
- A firewall backend –
nftables(default on Ubuntu 24.04 and Debian 12),iptables,firewalld, orufw. We use nftables. - An SSH server already running (OpenSSH), ideally with key-based auth enabled.
- Optional: Nginx for the web-application jails in Step 7, and a working mail transfer agent (Postfix) if you want the email alerts in Step 10.
One non-negotiable safety note before you start: keep a second, already-authenticated SSH session open, or have out-of-band console access through your cloud provider, for the entire tutorial. Fail2ban bans IP addresses, and the single most common mistake is banning your own. With a fallback session open, a self-inflicted ban is a 30-second fix instead of a support ticket.
Step 1 – Install Fail2ban and Confirm the Version
Update your package index and install fail2ban from the distribution repository. On Ubuntu or Debian:
# Ubuntu 24.04 / Debian 12
sudo apt update
sudo apt install -y fail2ban
# RHEL 9 / Rocky / AlmaLinux (needs EPEL)
# sudo dnf install -y epel-release
# sudo dnf install -y fail2ban
# Confirm the installed version
fail2ban-client version
The fail2ban-client version command prints the exact build you are running – for example 1.0.2 on Ubuntu 24.04’s repository, 1.1.0-9 if you’re tracking Debian unstable as of December 2025, or 1.1.0-16 on a Fedora 44 box rebuilt in January 2026. Upstream itself has moved on to 1.1.1, released in August 2026 and now archived by Zenodo alongside GitHub’s own release history, which counted 29 tagged releases for the project that same month; the German Wikipedia entry and the Fossies source-tarball index (a 627,126-byte archive whose single ChangeLog file is also dated August 2026) both list 1.1.1 as current. Admins are already running it in production – the sysadmin behind notes.sagredo.eu documented upgrading their mail server’s fail2ban install to 1.1.1 in an August 2026 changelog entry. All of these are production-ready; the 1.1.x series added native nftables actions, incremental ban times, and improved systemd-journal support, and SourceForge’s own release notes credit 1.1.1 specifically with a new systemd-managed fail2ban.socket unit as of that same August 2026 release, all of which we use later. Do not chase the bleeding edge here. The repository build receives security patches through your normal update channel, which is exactly what you want for a security tool.
Installation pulls in a systemd service called fail2ban.service. On Debian and Ubuntu it is enabled and started automatically with a single default jail (sshd) already active. Verify the service is alive before moving on:
sudo systemctl status fail2ban --no-pager
# Expect: Active: active (running)
# See which jails are already enabled out of the box
sudo fail2ban-client status
If fail2ban-client status already lists sshd under “Jail list,” the default install is protecting SSH right now. The remaining steps replace those defaults with a deliberate, documented configuration you control.
Step 2 – Create jail.local (Never Edit jail.conf)
Fail2ban ships its defaults in /etc/fail2ban/jail.conf. Never edit that file. Package upgrades overwrite it without warning, and your carefully tuned settings vanish. Instead, fail2ban reads jail.local after jail.conf and lets local values win. Anything you put in jail.local survives upgrades. This single convention is the most common fail2ban pitfall, and the fix is to create your own file from the start:
# Create an empty local override file (do NOT copy the whole jail.conf)
sudo install -m 644 /dev/null /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Copying the entire 900-line jail.conf into jail.local is a trap: you inherit every default and then fight to find your changes. Keep jail.local small. It should contain only a [DEFAULT] section with your global policy plus the specific jails you want enabled. We build that [DEFAULT] block in the next step. The same rule applies to individual jails: per-jail overrides go in jail.local or in separate files under /etc/fail2ban/jail.d/, never in the shipped .conf files.
If you prefer a modular layout, you can drop one file per service into /etc/fail2ban/jail.d/ – for example jail.d/sshd.local and jail.d/nginx.local. Fail2ban merges every *.local and *.conf file in that directory alphabetically. For a single server, one tidy jail.local is easier to audit, so that is what we use here.
Step 3 – Set Safe Global Defaults
The [DEFAULT] section sets policy for every jail unless a jail overrides it. These are the four knobs that decide how aggressive fail2ban is, plus the all-important whitelist. Fail2ban’s shipped defaults are deliberately gentle: bantime = 10m, findtime = 10m, and maxretry = 5. For an internet-facing box, tighten them. Paste this into the top of your jail.local:
[DEFAULT]
# Ignore your own networks so you can never ban yourself.
# Replace 203.0.113.10 with your real office / home IP.
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
# How long an IP stays banned after crossing the threshold
bantime = 1h
# The sliding window in which failures are counted
findtime = 10m
# Failures allowed inside findtime before a ban
maxretry = 5
# Read logs from the systemd journal (Ubuntu 24.04 default)
backend = systemd
# Use nftables to enforce bans (see Step 5)
banaction = nftables-multiport
banaction_allports = nftables-allports
# Ban both IPv4 and IPv6 attackers
allowipv6 = auto
The ignoreip line is your safety net – list every address you administer from, including your VPN egress and any monitoring or CI runners that hit the server. The table below explains each parameter and the value we recommend versus the shipped default.
| Parameter | Shipped default | Recommended | What it controls |
|---|---|---|---|
bantime | 10m | 1h (or longer) | How long a banned IP is blocked |
findtime | 10m | 10m | Window in which failures are counted |
maxretry | 5 | 3-5 | Failures allowed before a ban |
backend | auto | systemd | How logs are read on journald systems |
ignoreip | 127.0.0.1/8 ::1 | + your admin IPs | Addresses that are never banned |
banaction | iptables-multiport | nftables-multiport | Firewall used to enforce bans |
A word on backend = systemd: on Ubuntu 24.04, SSH and most services log to the systemd journal rather than to a flat file like /var/log/auth.log. If you leave fail2ban on a file backend, the SSH jail will read an empty or non-existent log and silently ban nobody. Setting backend = systemd in [DEFAULT] makes every jail read the journal by default, which is the correct choice on modern distributions.
Step 4 – Harden the SSH Jail
SSH is the jail that matters most, so configure it explicitly rather than relying on the package default. Add an [sshd] block to jail.local below your [DEFAULT] section:
[sshd]
enabled = true
port = ssh
backend = systemd
# Be stricter than the global default for SSH
maxretry = 3
findtime = 10m
bantime = 4h
# Catch both failed passwords and aggressive pre-auth probes
mode = aggressive
Two settings deserve a closer look. The mode = aggressive option tells the bundled sshd filter to match a wider set of failure patterns – not just “Failed password,” but also connection drops before authentication and malformed protocol attempts that bots use to probe. For a hardened server this is usually what you want. If you run unusual SSH clients and see false positives, drop back to the default mode = normal.
The port = ssh line resolves to port 22 via /etc/services. If you run SSH on a non-standard port, set the number directly (for example port = 2222) so the firewall ban targets the right port. Note that moving SSH off port 22 reduces log noise but is not a security control on its own – fail2ban is what actually stops the determined attackers who scan all ports. If you keep SSH on 22, that is fine; just make sure maxretry is tight, because port 22 takes the heaviest fire.
For the strongest posture, combine this jail with key-only authentication. Set PasswordAuthentication no in /etc/ssh/sshd_config, and brute-force attempts can no longer succeed even before fail2ban bans them – the jail then simply trims the log noise and blocks the scanners. Defence in depth means the jail is your second line, not your only one.
Step 5 – Choose Your Firewall Backend: nftables, iptables, or ufw
Fail2ban does not block traffic itself – it asks your firewall to do it through the banaction. Picking the wrong banaction is a classic failure mode: fail2ban reports an IP as “banned,” but the attacker keeps connecting because the rule was written into a firewall the kernel is not using. Modern Ubuntu and Debian default to nftables, so nftables-multiport (set in Step 3) is the right choice. The table below maps each common environment to its banaction.
| banaction | Firewall | Best for | Notes |
|---|---|---|---|
nftables-multiport | nftables | Ubuntu 24.04, Debian 12, Fedora | Modern default; uses native nft sets |
iptables-multiport | iptables (legacy) | Older servers, CentOS 7 | Still works via iptables-nft shim |
firewallcmd-rich-rules | firewalld | RHEL/Rocky/AlmaLinux | Use when firewalld is the manager |
ufw | ufw | Ubuntu hosts using ufw | Bans appear in ufw status |
route | kernel routing | Null-routing without a firewall | Blackholes the IP at the route table |
If your server already uses ufw as its firewall manager, do not mix banaction types – set banaction = ufw in [DEFAULT] so bans and your normal rules live in one place. Mixing nftables-multiport with an active ufw configuration leads to rules in two tables and the confusing situation where ufw status looks clean but the attacker is (or is not) actually blocked. Decide on one enforcement layer. For a server managed mostly by your cloud security group plus fail2ban, raw nftables-multiport is clean and fast.
Whichever you choose, fail2ban creates its own table or chain (commonly an f2b-sshd set) and adds the banned IPs there, so it never clobbers your existing rules. You can confirm the kernel is enforcing the ban directly, which we do in Step 11.
Step 6 – Start, Enable, and Verify with fail2ban-client
With jail.local written, test the configuration before reloading. Fail2ban can validate its config without touching the running service:
# Dry-run the full configuration and exit (catches syntax/regex errors)
sudo fail2ban-client -d > /dev/null && echo "Config OK"
# Apply the new configuration without a full restart
sudo fail2ban-client reload
# Make sure the service starts on every boot
sudo systemctl enable --now fail2ban
# Check overall status and the sshd jail specifically
sudo fail2ban-client status
sudo fail2ban-client status sshd
The per-jail status is where you confirm the jail is actually watching something. A healthy sshd jail looks like this:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 347
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 4
|- Total banned: 58
`- Banned IP list: 45.142.120.18 193.32.162.45 ...
“Total failed” climbing means the filter is matching real log lines – proof the backend and filter are wired correctly. “Total banned” climbing means the action is firing. If “Total failed” stays stuck at 0 on a server that is visibly being probed, your backend or log path is wrong; jump to the troubleshooting section. The fail2ban-client tool is your control panel for the rest of this tutorial, so keep the cheat sheet below handy.
| Command | What it does |
|---|---|
fail2ban-client status | List all active jails |
fail2ban-client status sshd | Show failures and bans for one jail |
fail2ban-client set sshd banip 1.2.3.4 | Manually ban an IP |
fail2ban-client set sshd unbanip 1.2.3.4 | Manually unban an IP |
fail2ban-client unban --all | Clear every active ban |
fail2ban-client reload | Re-read config without dropping bans |
fail2ban-client get sshd bantime | Read a jail’s current setting |
Step 7 – Protect Nginx and Web Applications
SSH is not the only service bots hammer. If you run a web server, your access and error logs fill with credential stuffing, vulnerability scanners, and bot floods. Fail2ban ships filters for the common Nginx cases – you just enable the jails. Add these blocks to jail.local:
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
backend = polling
[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
backend = polling
maxretry = 2
[nginx-limit-req]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
backend = polling
Note the explicit backend = polling and logpath here: Nginx writes to flat files, not the systemd journal, so these jails override the global systemd backend. The nginx-http-auth jail catches failed HTTP Basic Auth attempts, nginx-botsearch bans scanners probing for known-vulnerable paths, and nginx-limit-req works with Nginx’s own limit_req directive to ban clients that trip your rate limit. Together they cut the bulk of automated web abuse.
One critical caveat for sites behind a CDN or load balancer: if every request reaches Nginx from the proxy’s IP, fail2ban will read that proxy address from the log and ban your own CDN, knocking your whole site offline. The fix is to log the real client IP. Configure Nginx with the real_ip module (or read X-Forwarded-For) so your access log records the visitor, not the proxy – then fail2ban bans the right address. Test this on a staging server before enabling web jails in production. This same “ban the proxy by mistake” hazard is why teams running edge infrastructure pair fail2ban with platform-level controls; for a wider security stack comparison see our review of Bitdefender vs Norton vs McAfee.
Step 8 – Write and Test a Custom Filter with fail2ban-regex
Eventually you will need to ban a pattern fail2ban does not ship a filter for – a custom app login, a SaaS webhook abuse, or the perennial WordPress wp-login.php and xmlrpc.php flood. Writing a filter is two parts: a regex that matches the bad line and pulls out the IP via the <HOST> token, and a test to prove it works. Create the filter file first:
# /etc/fail2ban/filter.d/wordpress-auth.conf
[Definition]
# Match POST/GET hits to WordPress login and XML-RPC endpoints
failregex = ^<HOST> -.*"(GET|POST) /(wp-login.php|xmlrpc.php).*"
ignoreregex =
The <HOST> token is mandatory – it is how fail2ban extracts the IP to ban. Without it, even a regex that matches will never produce a ban. Before you wire this into a jail, test it against your real log with the fail2ban-regex tool. This is the single most valuable habit in the whole fail2ban tutorial, because it tells you exactly how many lines your filter catches:
sudo fail2ban-regex /var/log/nginx/access.log
/etc/fail2ban/filter.d/wordpress-auth.conf
The output summarises matches so you can tune the regex before any IP gets banned:
Running tests
=============
Use failregex line : ^<HOST> -.*"(GET|POST) /(wp-login.php|xmlrpc...
Use log file : /var/log/nginx/access.log
Results
=======
Failregex: 128 total
|- #) [# of hits] regular expression
| 1) [128] ^<HOST> -.*"(GET|POST) /(wp-login.php|xmlrpc.php).*"
`-
Lines: 9043 lines, 0 ignored, 128 matched, 8915 missed
“128 matched” confirms the filter is finding the attack traffic. If you see “0 matched” on a log you know contains attacks, the regex or the date pattern is off – adjust and re-run until the count is right. Once the test passes, add the jail that uses it:
[wordpress-auth]
enabled = true
port = http,https
filter = wordpress-auth
logpath = /var/log/nginx/access.log
backend = polling
maxretry = 5
findtime = 5m
bantime = 24h
Reload fail2ban (sudo fail2ban-client reload) and the new jail goes live. The discipline of “write regex, test with fail2ban-regex, then enable” prevents the two worst custom-filter outcomes: a filter that bans nothing, and a filter that bans everything including legitimate users.
Step 9 – Add the Recidive Jail for Repeat Offenders
Persistent attackers wait out a one-hour ban and come straight back. The recidive jail solves this elegantly: it watches fail2ban’s own log file and bans any IP that has been banned by other jails too many times, applying a much longer sentence. It is fail2ban policing its own repeat customers. Add it to jail.local:
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
backend = polling
banaction = nftables-allports
# Ban anyone re-banned 5 times within a day...
maxretry = 5
findtime = 1d
# ...for a full week, across every port
bantime = 1w
Two details make the recidive jail effective. First, it reads /var/log/fail2ban.log, so fail2ban must be logging to that file – on Debian/Ubuntu it does by default, but if you switched logtarget to the journal, point the jail at the journal instead or restore file logging. Second, the banaction = nftables-allports blocks the repeat offender on every port, not just the one they attacked. Someone who has failed SSH and WordPress and HTTP Basic Auth five times in a day has demonstrated bad intent across the board, so an all-ports week-long ban is proportionate.
The recidive jail turns fail2ban from a short-term annoyance into a real deterrent. A scanner that probes you, gets a one-hour ban, returns, and repeats will accumulate recidive strikes and eventually disappear for a week. Combined with the persistent ban database (Step 12), even a reboot will not give them a clean slate. This escalation model – short bans that compound into long ones for the worst actors – is exactly how production servers keep brute-force noise near zero.
Step 10 – Configure Email Alerts and Whitelisting
Silent bans are fine, but for SSH you usually want to know when something gets blocked. Fail2ban has built-in action shortcuts that combine a ban with an email. The richest is action_mwl – it bans the IP, then emails you a WHOIS lookup of the attacker plus the matching log lines. Set it globally in your [DEFAULT] section:
[DEFAULT]
# ...your existing defaults from Step 3...
destemail = [email protected]
sender = [email protected]
mta = sendmail
# Ban + email with whois + relevant log lines
action = %(action_mwl)s
This requires a working mail transfer agent. Install one with sudo apt install -y postfix mailutils and choose “Internet Site” during setup, or point mta at an existing relay. If you do not want email at all, leave the default action = %(action_)s, which bans without notifying. A middle ground is action_mw (ban + email with WHOIS but no log lines), which is lighter on noise.
Whitelisting deserves a second pass here because it is where lockouts happen. Beyond the ignoreip list from Step 3, fail2ban honours ignoreself = true (on by default), which prevents the server from banning its own IPs. If you administer from a dynamic home IP, do not try to chase it in ignoreip – instead connect through a stable jump host or VPN whose egress IP you can safely whitelist. For teams provisioning infrastructure as code, bake the admin and CI IP ranges into your ignoreip via your config-management tool so a rebuild never forgets them – Puppet Forge’s official module reached version 4.1.0 in May 2025 and the Sous-Chefs Chef cookbook hit version 8.0.0 in April 2026, so both ecosystems have current, maintained options; if you manage cloud infra with Terraform, our Terraform AWS tutorial shows the pattern for templating values like these.
Step 11 – Simulate an Attack, Watch a Ban, and Unban
Configuration you have not tested is configuration you do not have. With your fallback session still open, trigger a real ban from a different IP – a phone on cellular data, a second VPS, or a cloud shell. Attempt SSH with a wrong username several times past your maxretry:
# From a DIFFERENT machine/IP, fail the login on purpose:
for i in 1 2 3 4; do ssh [email protected]; done
# Back on the server, watch the ban land:
sudo fail2ban-client status sshd
# Confirm the kernel is actually enforcing it (nftables):
sudo nft list ruleset | grep -A4 f2b
Within seconds the banned IP appears in the jail status and in the nftables set. From the attacking machine, further SSH attempts now hang and time out – proof the ban is real at the packet level, not just logged. The fail2ban log records each event in a readable line:
2026-06-26 11:42:07,318 fail2ban.actions [812]: NOTICE [sshd] Ban 198.51.100.77
2026-06-26 12:42:08,011 fail2ban.actions [812]: NOTICE [sshd] Unban 198.51.100.77
To release a ban early – for instance when you ban yourself during testing – use the unban command. This is the exact recovery you would run from a console session if a self-inflicted ban locked you out:
# Unban a single IP
sudo fail2ban-client set sshd unbanip 198.51.100.77
# Or clear every ban across all jails
sudo fail2ban-client unban --all
Run this test once on every server you deploy. It takes two minutes and it is the only way to be certain the entire chain – backend reads the log, filter matches, jail counts, action bans, firewall enforces – actually works on this specific machine. A jail that looks enabled but never bans is worse than no jail, because it gives false confidence.
Step 12 – Harden for Production: Persistent Bans and Monitoring
The final step turns a working setup into a production-grade one. Three things matter: bans must survive reboots, attackers should pay an escalating price, and you need visibility. Fail2ban already persists active bans in its SQLite database (/var/lib/fail2ban/fail2ban.sqlite3) and re-applies any unexpired ban on restart, so a reboot does not hand attackers a clean slate. Confirm persistence is on (it is by default) and tune the purge age in [DEFAULT]:
[DEFAULT]
# Keep ban records long enough to outlive your longest bantime
dbpurgeage = 14d
# Escalating bans: each repeat offence multiplies the ban time
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 5w
The bantime.increment feature is one of the best reasons to be on the 1.1.x series, and the packaging trail shows how actively that branch is maintained: Fedora rebuilt fail2ban 1.1.0-12 against Python 3.14.0rc3 in August 2025, maintainer Orion Poplawski cleaned up the spec file in a 1.1.0-14 update that October, and the same rebuild cadence carried into 2026 when Fedora’s Python maintainer pushed fail2ban 1.1.0-18 to track Python 3.15 in June 2026. In practice, a first offence gets your base bantime, a second doubles it, a third doubles again, up to bantime.maxtime. Persistent scanners dig themselves into multi-week bans automatically. For monitoring, wire fail2ban into whatever you already use. The quickest checks are a daily ban count and the live log:
# How many IPs are banned right now, across all jails?
sudo fail2ban-client banned
# Follow bans in real time
sudo journalctl -u fail2ban -f
# Count today's bans for a quick daily metric
grep "$(date +%Y-%m-%d)" /var/log/fail2ban.log | grep -c 'Ban '
For long-term operation, make sure log rotation does not break your jails: the logrotate package ships a fail2ban entry that reloads the service after rotating /var/log/fail2ban.log, so the recidive jail keeps reading a valid file. Finally, schedule a monthly review of your banned-IP trends – a sudden spike often signals a new campaign worth investigating, the same early-warning instinct that helps teams spot large-scale incidents like the Canvas LMS breach that exposed 275 million records before they spread.
Fail2ban vs CrowdSec vs UFW: Which Do You Actually Need?
A common point of confusion is whether fail2ban replaces a firewall or a newer tool like CrowdSec. It replaces neither – they solve different layers, and the strongest setups use them together. A firewall (ufw, nftables, or your cloud security group) decides which ports are open. Fail2ban decides which IPs abusing those open ports get temporarily blocked. CrowdSec does what fail2ban does but adds a crowd-sourced threat-intelligence network: agents share attacker IPs so you can block addresses that hit other people’s servers before they reach yours.
| Dimension | Fail2ban | CrowdSec | UFW |
|---|---|---|---|
| Primary role | Log-based IP banning | Log-based banning + shared blocklist | Firewall front-end |
| Written in | Python | Go | Python wrapper for nftables/iptables |
| Threat intel | Local only | Crowd-sourced network | None |
| GitHub stars | 18,000+ | 14,000+ | Part of Ubuntu |
| License | GPL | MIT | GPL |
| Best when | Single/few servers, simple, proven | Fleets wanting shared intelligence | You just need to manage open ports |
For most individuals and small teams, fail2ban remains the right first choice: it is simpler, ships everywhere, has no external dependency, and the configuration you built in this tutorial is auditable in one file. CrowdSec earns its place once you run a fleet and want collaborative blocking. The two can even coexist. UFW, meanwhile, is not an alternative to either – it is the firewall fail2ban can enforce bans through. Choose your enforcement layer, add fail2ban for behavioural banning, and layer CrowdSec on top only when shared intelligence is worth the extra moving part.
Common Pitfalls That Break Fail2ban
Most fail2ban “it doesn’t work” complaints trace back to a handful of avoidable mistakes. Here are the six that cause the most lost time:
- Editing jail.conf instead of jail.local. Your changes vanish on the next package upgrade. Always put overrides in
jail.localorjail.d/. - Wrong backend for your log source. On Ubuntu 24.04, SSH logs to the journal. A file backend reading
/var/log/auth.logmatches nothing and bans no one. Usebackend = systemdfor journald services. - Forgetting to whitelist yourself. If your admin IP is not in
ignoreip, one fat-fingered password streak can lock you out. Add your stable egress IP before going live. - Banaction/firewall mismatch. Setting
iptables-multiporton an nftables-only host (or fighting an active ufw) means fail2ban “bans” IPs that keep connecting. Match the banaction to your real firewall. - Custom filter without the
<HOST>token. A regex that matches but omits<HOST>can never ban anyone. Always test new filters withfail2ban-regex. - Banning your own proxy. Behind a CDN or load balancer, logging the proxy IP instead of the real client makes fail2ban ban your own infrastructure. Configure real-client-IP logging first.
Troubleshooting Fail2ban: 8 Fixes That Work
When a jail misbehaves, work through these in order. Each maps a symptom to its usual cause and fix.
- Service won’t start. Run
sudo fail2ban-client -dto dump and validate the config. A single bad regex or missing log path will abort startup; the dump points to the offending jail. - Jail shows 0 failures during an obvious attack. The backend or log path is wrong. Verify with
fail2ban-regexagainst the actual log, and confirmbackend = systemdfor journald services. - “Total banned” rises but attackers still connect. The banaction is writing to a firewall the kernel is not using. Check
sudo nft list rulesetfor thef2bset and match your banaction to the active firewall. - “ERROR NOK: (‘sshd’,)” on reload. The sshd jail references a log file that does not exist. On journald systems set the jail’s
backend = systemdrather than pointing at/var/log/auth.log. - Bans gone after a reboot. The service was not enabled, or the database is unwritable. Run
systemctl enable fail2banand check permissions on/var/lib/fail2ban/. - You locked yourself out. Use console/out-of-band access, then
fail2ban-client set sshd unbanip YOUR_IPand add your IP toignoreipso it cannot recur. - Email alerts never arrive. No working MTA. Install
postfixandmailutils, test withecho test | mail [email protected], and checkmtain[DEFAULT]. - IPv6 attackers not banned. Confirm
allowipv6 = autoand that your nftables ruleset has aninet(dual-stack) table so v6 bans take effect.
Advanced Tips for Production Fail2ban
Once the basics are solid, a few advanced patterns make fail2ban meaningfully stronger. Report attackers upstream: fail2ban ships an AbuseIPDB action (action.d/abuseipdb.conf) that submits banned IPs to the public abuse database, turning your server into a sensor that helps everyone. Ban at the edge: if your site sits behind Cloudflare, the bundled Cloudflare action can block attackers at the CDN edge using the real visitor IP, so malicious traffic never reaches your origin. Tighten the SSH window: combining maxretry = 2 with bantime.increment = true makes the first probe cheap to forgive but the second offence escalate fast.
For containerised workloads, remember that fail2ban runs on the host, not inside the container – give it read access to the container’s mounted logs and the host’s firewall, and it protects Dockerised services just as well. If you would rather run fail2ban itself as a container instead of a host service, LinuxServer.io maintains a prebuilt image – rebased onto Alpine 3.21 in February 2025 and now on build 1.1.0-r2-ls10 as of April 2025 – though the host-service approach above stays the simpler default for most setups. Finally, treat your fail2ban config as code: keep jail.local, your custom filters, and your ignoreip ranges in version control and deploy them with your configuration-management tooling so every new server inherits the same hardened baseline on day one. A VPS without fail2ban in 2026 is a VPS volunteering its CPU to the next P2PInfect outbreak; a few minutes of config is the cheapest security win you will make all quarter. Pair it with strong authentication everywhere – including a trustworthy VPN, as we cover in our X-VPN review – and the brute-force surface effectively disappears.
Related Coverage
More Security and Infrastructure Guides
- ShinyHunters Hit 100+ Orgs via Oracle Zero-Day [2026] – why patching and access control matter alongside fail2ban.
- Canvas LMS Breach: 275M Records, 9K Schools Hit [2026] – the scale of modern credential-driven incidents.
- Bitdefender vs Norton vs McAfee 2026 – endpoint protection to layer on top of server hardening.
- Terraform Tutorial: Provision AWS in 13 Steps [2026] – template your hardened server baseline as code.
- X-VPN Review 2026: Can You Trust Its No-Logs Policy? – secure the admin path into your servers.
- Cybersecurity Threats 2026 – our full hub on the year’s attacks, tools, and defences.
Frequently Asked Questions About Fail2ban
Is fail2ban still relevant in 2026 with cloud firewalls?
Yes. Cloud security groups control which ports are open but cannot tell a legitimate failed login from a brute-force flood on those open ports. Fail2ban fills that gap by banning IPs based on behaviour. With honeypot data showing worms like P2PInfect driving 80%+ of SSH attacks, behavioural banning is as relevant as ever.
Does fail2ban replace a firewall like ufw?
No. Fail2ban uses a firewall (nftables, iptables, firewalld, or ufw) to enforce its bans – it does not replace one. You still need a firewall to define which ports are open. Fail2ban adds a dynamic layer that temporarily blocks individual abusive IPs through that firewall.
What’s the difference between fail2ban and CrowdSec?
Both ban IPs based on log analysis. Fail2ban is local-only, written in Python, and dead simple to run on one server. CrowdSec is written in Go and adds a crowd-sourced blocklist so you benefit from attacks seen across the whole network. For a few servers, fail2ban is simpler; for fleets wanting shared threat intelligence, CrowdSec adds value.
How do I whitelist my own IP so I don’t get banned?
Add your address to the ignoreip directive in the [DEFAULT] section of jail.local, for example ignoreip = 127.0.0.1/8 ::1 203.0.113.10. Use a stable IP such as a VPN or jump-host egress rather than a dynamic home address. Reload with fail2ban-client reload for it to take effect.
Where are fail2ban’s logs and ban database?
The action log is /var/log/fail2ban.log (or the systemd journal via journalctl -u fail2ban), and active bans are stored in the SQLite database at /var/lib/fail2ban/fail2ban.sqlite3. That database is what lets fail2ban restore unexpired bans after a reboot.
What are good bantime, findtime, and maxretry values?
A solid starting point for an exposed server is maxretry = 3, findtime = 10m, and bantime = 1h for SSH, with bantime.increment = true so repeat offenders escalate automatically. The shipped defaults (maxretry 5, 10-minute ban) are gentler; tighten them for public-facing boxes.
Does fail2ban work with Docker containers?
Yes, but run it on the host rather than inside the container. Give the host fail2ban read access to the container’s mounted log files and control of the host firewall. It then bans abusive IPs hitting your Dockerised services using the host’s nftables or iptables rules.
How do I permanently ban or unban all IPs?
For a permanent ban set bantime = -1 in a jail. To clear bans, use fail2ban-client set <jail> unbanip <ip> for one address or fail2ban-client unban --all to wipe every active ban across all jails. The recidive jail is usually a better long-term answer than permanent bans.
Configuration paths and defaults reflect fail2ban 1.1.x on Ubuntu 24.04 LTS as of August 2026, with upstream at 1.1.1 – the version listed by the German Wikipedia entry and the Fossies source-tarball index as of that month. Always confirm your installed version with fail2ban-client version: SentinelOne reported in June 2026 that CVE-2025-45311 affects older fail2ban-client 0.11.2 builds still lingering on some legacy systems, though the fail2ban maintainers have disputed the severity of that finding – one more reason to stay current on the actively patched 1.1.x branch rather than an ancient install. Consult the official fail2ban repository and the Arch Wiki fail2ban page for distribution-specific paths.





