Fail2ban is a log-parsing intrusion prevention system that reads application logs, matches patterns against rules, and bans IPs via firewall rules. Its effectiveness depends entirely on configuration discipline—sloppy filters and trigger thresholds create either security theater or denial of service against legitimate users.
The Mental Model
Fail2ban operates in three layers:
- Filter: regex pattern that identifies attack attempts in logs
- Jail: threshold and time window that decides when to trigger
- Action: what to do when the threshold is crossed (usually add firewall rule)
A jail is useless if its filter is too loose (catches legitimate traffic) or too tight (misses attacks). A filter is useless if the jail threshold is set so high that attackers succeed before the ban fires. Actions are useless if they don't persist or if they're applied to the wrong chain.
Think of it as a three-stage gate: the filter decides what counts as an attempt, the jail decides how many attempts trigger the gate, and the action decides what happens when the gate closes.
Filter Design
Start with the log format you're defending. SSH logs look like this:
Jan 15 10:23:45 host sshd[1234]: Invalid user admin from 192.0.2.5 port 54321
A filter for this line needs to extract the IP. Fail2ban provides a <HOST> placeholder that matches common IP formats:
^.*Invalid user .* from <HOST> port \d+$
Gotcha: regex is anchored by default. If your log has prefix noise (timestamps, hostnames), anchor to the meaningful part or use a broader pattern. Test every filter against actual logs before deploying.
For web applications, log lines are messier. Nginx access logs include the remote IP in a fixed position:
192.0.2.10 - - [15/Jan/2025:10:23:45 +0000] "POST /login HTTP/1.1" 401
The filter extracts the first IP:
^<HOST> - - \[.*\] ".*" 401$
This is tighter than it needs to be (the 401 is optional), but tight filters are safer. Fail2ban ships with pre-built filters for common services (sshd, nginx, apache, postfix). Use them as templates, not gospel. Every application logs differently.
Gotcha: if you're filtering on HTTP 401 responses, you're catching failed logins. If you're filtering on 403, you're catching permission denials (which might be legitimate). Know the difference.
Jail Tuning
A jail defines three parameters:
- maxretry: how many matches before ban
- findtime: the window in seconds
- bantime: how long the ban lasts
Example:
[sshd]
enabled = true
maxretry = 5
findtime = 600
bantime = 3600
This means: ban an IP if it matches the filter 5 times within 600 seconds (10 minutes), and keep it banned for 3600 seconds (1 hour).
Tuning is a trade-off. Tight settings (low maxretry, short findtime) stop attackers fast but risk false positives. Loose settings (high maxretry, long findtime) tolerate user mistakes but let attackers probe longer.
For SSH, start with maxretry=5, findtime=600. For web login forms, start with maxretry=3, findtime=300 (web attackers move faster). For mail services, start with maxretry=10, findtime=3600 (legitimate clients retry more often).
Gotcha: bantime is absolute. If you set it to 86400 (24 hours), a user locked out at 9 AM stays locked until 9 AM the next day. This is intentional—long bans deter attackers. But it's also a customer support nightmare. Document the policy before deploying.
Action Chains
Actions are what Fail2ban does when a ban fires. The default is to add a rule to iptables (or nftables on modern systems):
[DEFAULT]
banaction = iptables-multiport
This adds an INPUT rule that drops all traffic from the banned IP. It's simple and works, but it's also blunt—you're blocking all ports, not just SSH.
For SSH, use banaction = iptables-ssh to block only port 22. For web services, use banaction = iptables-http to block only ports 80 and 443.
Gotcha: if you run fail2ban inside a container or behind a load balancer, iptables rules on the container won't affect external traffic. You need to apply bans at the edge (firewall or WAF). Fail2ban can integrate with cloud provider APIs (AWS security groups, Cloudflare) but that requires additional setup.
For persistence, bans are stored in /var/lib/fail2ban/fail2ban.sqlite3. If the service restarts, bans persist (unless you're using the default iptables action on a system without a firewall state file—then bans are lost). Verify this on your platform before relying on it.
Whitelist and Recidivism
Create an ignoreip list to exempt known-good IPs:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
This prevents false positives from your office, VPN, or monitoring systems. One often-overlooked risk here is that network-level threats don't always come from outside—the comparison on digitalwarga.id between public and private WiFi is a useful reminder that trusted-looking source IPs can still originate from hostile environments. Gotcha: if you add a CIDR block (like 203.0.113.0/24), fail2ban will never ban any IP in that range—including attackers on that network. Use narrow ranges or specific IPs only.
For repeat offenders, Fail2ban supports recidivism rules. After three bans within a week, extend the fourth ban to 7 days. This requires a separate jail with filter = recidivism, which is complex to configure and rarely worth it. Skip this unless you're seeing patterns.
Monitoring and Alerting
Fail2ban writes to syslog. Monitor these events:
Ban added: IP was banned (expected, but log it)Ban removed: ban expired (expected, but watch for patterns)Unban: manual removal (should be rare)Restored: bans reloaded after restart (expected on boot)
Set up alerts for:
- Same IP banned multiple times in one day (possible scanning campaign)
- Unusually high ban rate (filter might be too loose)
- Bans removed manually (investigate why)
Don't alert on every ban. Ban volume should be low (< 10/day on a typical server). If you're banning 100+ IPs daily, your filter is wrong or your service is under active attack.
Gotcha: fail2ban doesn't alert by default. You need to integrate it with your monitoring stack (Prometheus, Datadog, Splunk) or parse logs with a log aggregator. This is outside Fail2ban's scope but critical for production visibility.
When This Breaks
Fail2ban fails when:
- The filter doesn't match the log format (most common—logs changed, filter wasn't updated)
- The jail threshold is too high (attackers succeed before ban fires)
- The action doesn't work (firewall is misconfigured, or bans apply to the wrong interface)
- Whitelisting is too broad (legitimate users get banned anyway)
Test by manually triggering the filter. SSH into the server, fail a login 5 times, and verify the ban:
fail2ban-client set sshd unbanip 192.0.2.5
fail2ban-client status sshd
If the IP isn't in the banned list, the filter didn't match. Check the logs:
tail -f /var/log/fail2ban.log
One-Line Takeaway
Fail2ban configuration best practices: tight filters matched to your log format, conservative thresholds (low maxretry, short findtime) that trade speed for accuracy, action chains that target only the service under attack, and persistent monitoring to catch filter drift.