fail2ban
SSH Hardening mentioned fail2ban briefly, as one piece of a broader SSH checklist: it watches logs, and bans an IP address that produces too many failed attempts within a time window. This article is the full treatment — fail2ban isn't SSH-specific at all; it's a general log-watching framework that can protect any service with a log file and a recognizable failure pattern, from a mail server to a web application's login form.
What You Will Learn
By the end of this article you will be able to:
- explain how fail2ban turns a log pattern into a firewall ban;
- configure a jail in
jail.localwithout editing the shipped defaults directly; - test a filter's regex against real log lines before trusting it in production;
- check, unban, and whitelist addresses by hand;
- protect a second service beyond SSH with its own jail.
How fail2ban Actually Works
fail2ban has three moving parts that combine into one jail:
- a log file to watch (
/var/log/auth.log, an application's own log, and so on); - a filter — a set of regular expressions that recognize a failed-attempt line in that log;
- an action — what to do once the filter has matched too many times from one address within a time window, almost always inserting a firewall rule to block it.
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled)
Active: active (running)
fail2ban itself doesn't replace the firewall — it manipulates it, inserting and removing ban rules dynamically as a nftables/iptables set (see iptables and nftables) that UFW or a raw nftables ruleset then still enforces underneath.
Never Edit the Shipped Defaults Directly
fail2ban ships with a default configuration in /etc/fail2ban/jail.conf, but that file is expected to be overwritten on the next package upgrade — any local customization belongs in /etc/fail2ban/jail.local, which fail2ban reads after jail.conf and which any setting there overrides:
Copying the whole file as a starting point is the conventional approach, but only the sections actually being customized need to remain — anything left unset falls back to jail.conf's default automatically.
Configuring a Jail
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 203.0.113.5
[sshd]
enabled = true
port = 22
logpath = %(sshd_log)s
maxretry = 3
bantime— how long a ban lasts once triggered (1h, or-1for permanent);findtime— the rolling window within whichmaxretryfailures must occur to trigger a ban;maxretry— how many matching failures withinfindtimetrigger the ban;ignoreip— addresses (and CIDR ranges) never banned regardless of how they match — always include the management workstation's own address here, since a ban on your own IP is exactly as effective at locking you out as one on an attacker's;[sshd]overridesmaxretryspecifically for this jail, stacking on top of the[DEFAULT]section's values for everything not explicitly restated.
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 14
| `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
|- Currently banned: 1
|- Total banned: 3
`- Banned IP list: 198.51.100.7
Testing a Filter Before Trusting It
A filter that fails to match real log lines silently protects nothing, with no error to indicate the problem — it's worth verifying a filter's regex actually matches before relying on it:
Results
=======
Success, the total number of match is 14
Overall
=======
Lines: 20348 lines, 0 ignored, 14 matched, 20334 missed
fail2ban-regex runs the filter against the actual log file without banning anyone — the safe way to confirm a custom filter (or a modified one, after a distribution changes its log format) still matches before depending on it in production.
Checking, Unbanning, and Whitelisting
Unbanning is the correct response to a false positive — a legitimate user who mistyped their password too many times, for instance — rather than restarting the whole fail2ban service, which would also clear every other active ban unrelated to the mistake.
Permanent whitelisting for addresses that should never trigger a ban belongs in ignoreip, not repeated manual unbanning:
Protecting a Second Service
fail2ban's design generalizes past SSH — any service with a log file and a recognizable failure line can get its own jail. A common example: banning repeated failed HTTP basic-auth attempts against an nginx site.
fail2ban ships a matching filter (/etc/fail2ban/filter.d/nginx-http-auth.conf) for this exact log pattern already — the jail definition only needs to enable it and point at the right log path, since the filter itself is part of the standard distribution.
Status for the jail: nginx-http-auth
|- Filter
| |- Currently failed: 0
| |- Total failed: 6
| `- File list: /var/log/nginx/error.log
`- Actions
|- Currently banned: 0
|- Total banned: 1
`- Banned IP list:
How This Relates to pam_faillock and UFW's limit
Three mechanisms in this course address a similar-sounding problem, and being able to distinguish them precisely is worth preparing for directly:
| Mechanism | Reacts to | Scope of the block |
|---|---|---|
ufw limit (see Firewall and UFW) |
Raw connection rate, no awareness of success/failure | Per source IP, at the firewall, before authentication is even attempted |
pam_faillock (see Root and sudo) |
Consecutive authentication failures for one account | Per account, regardless of source IP |
| fail2ban | Failure patterns in a log file, service-agnostic | Per source IP, at the firewall, based on genuinely failed attempts |
Interview angle: why deploy all three, not just one
A distributed brute-force attack spreading a few attempts per account across thousands of source IPs defeats fail2ban's per-IP banning (no single IP ever crosses the threshold) but not pam_faillock's per-account lockout; a slow, patient attacker rotating through many usernames from a single IP defeats pam_faillock's per-account counting but not fail2ban's per-IP banning. ufw limit catches neither pattern precisely but adds a coarse, cheap first line of defense that blocks obvious high-speed scanning before it even reaches the authentication layer. None of the three alone covers every attack shape — that's the practical argument for deploying all three together, not a redundancy to be trimmed.
Practical Scenario: Diagnosing a Jail That Isn't Banning Anything
Problem. SSH is clearly under active attack (dozens of failed logins visible in auth.log), but fail2ban-client status sshd shows zero bans.
Step 1 — confirm the jail is even enabled and watching the right log:
The jail doesn't exist at all — [sshd] was never set to enabled = true in jail.local, so jail.conf's own default (disabled, on this distribution) is what's actually in effect.
Step 2 — enable it and confirm the log path matches reality:
Step 3 — verify the filter is actually matching the log format in use:
Conclusion. The jail was simply never enabled — a configuration gap, not a filter or firewall problem. Once enabled and confirmed to match real log lines, subsequent failed attempts start accumulating toward maxretry correctly.
Common Mistakes
- Editing
jail.confdirectly. The next package upgrade overwrites it, silently discarding the customization; all local changes belong injail.local. - Forgetting to add the management workstation's own IP to
ignoreip. A single mistyped password from a shared office IP can lock out an entire team, not just the person who mistyped it. - Assuming a jail is enabled just because it's listed in the config file. As shown in the scenario above, a jail section can exist without
enabled = true, in which case it does nothing. - Trusting a filter without testing it against real log lines. A log format change after a distribution upgrade can silently break a previously working filter —
fail2ban-regexis the way to confirm it still matches before relying on it. - Using fail2ban as the only defense against brute-force login attempts. As covered above, it addresses a different attack shape than
pam_faillockandufw limit; relying on it alone leaves specific attack patterns uncovered.
Exercises
- On a lab VM, enable the
sshdjail with a lowmaxretry(2) and a shortbantime(60), then deliberately fail SSH login from another machine or terminal enough times to trigger a ban. Confirm withfail2ban-client status sshdand unban yourself afterward. - Add your own management IP to
ignoreipand explain, in one sentence, why this line is as important for availability as the jail itself is for security. - Run
fail2ban-regexagainst a service's log file and its corresponding filter, and interpret the "matched" versus "missed" counts in the output. - For a hypothetical mail server under a password-spraying attack (many accounts, few attempts each, from one IP), explain which of fail2ban,
pam_faillock, andufw limitwould catch it first, and why.
Summary
fail2ban watches a log file for a recognizable failure pattern and bans the offending address at the firewall once a threshold is crossed within a time window — a general mechanism, not an SSH-specific one, configured through jail.local rather than the overwritable jail.conf. It complements, rather than replaces, pam_faillock's per-account lockout and UFW's raw rate limiting, since each of the three catches a different attack shape. The next article, auditd Basics, covers recording security-relevant kernel events directly — a finer-grained, more general form of the same "watch, then act" idea.