TL;DR — Quick Summary
Fail2Ban complete guide: install, configure SSH jails, progressive banning, custom filters, Nginx/Apache/mail protection, and Cloudflare integration on Linux.
Every Linux server exposed to the internet is under constant attack. Automated bots scan the entire IPv4 address space within hours, testing thousands of username/password combinations against SSH and other services. Fail2Ban is a log-based intrusion prevention framework that detects these attacks in real time and instructs your firewall to block the offending IPs — automatically, without manual intervention. This guide covers the complete Fail2Ban setup: installation, SSH jail configuration, progressive banning, custom filter authoring, protection for Nginx, Apache, and mail servers, and Cloudflare API integration for edge-level blocking.
Prerequisites
Before you begin, make sure you have:
- A Linux server running Ubuntu 20.04/22.04/24.04, Debian 11/12, or RHEL/Rocky/AlmaLinux 8/9
- Root or sudo access
- SSH access to the server
- iptables, nftables, or firewalld already managing your firewall
- Basic familiarity with systemd service management
How Fail2Ban Works
Fail2Ban operates on a monitor → match → ban loop:
- Log monitoring — Fail2Ban’s
logtargetdaemon watches log files (e.g.,/var/log/auth.log,/var/log/nginx/error.log) using inotify or polling. - Regex matching — Each jail references a filter containing one or more
failregexpatterns. When a log line matches, Fail2Ban increments a failure counter for the source IP. - Threshold check — Once the counter reaches
maxretrywithin thefindtimewindow, the IP is considered hostile. - Firewall ban — Fail2Ban calls an action (typically iptables, nftables, or firewalld) to insert a DROP rule for the offending IP. The rule is automatically removed after
bantimeseconds.
Jails are defined in configuration files and combine a filter (what to match) with an action (what to do). The service stores its state in /var/lib/fail2ban/, so bans survive a service restart.
Installation
Ubuntu / Debian
sudo apt update
sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
RHEL / Rocky / AlmaLinux
The EPEL repository is required:
sudo dnf install epel-release -y
sudo dnf install fail2ban -y
sudo systemctl enable --now fail2ban
On RHEL-family systems, firewalld is the default backend. Install the optional integration:
sudo dnf install fail2ban-firewalld -y
Verify Fail2Ban is running:
sudo fail2ban-client status
Expected output:
Status
|- Number of jail: 1
`- Jail list: sshd
SSH Jail Configuration
Never edit /etc/fail2ban/jail.conf directly — it is overwritten on package upgrades. Instead, create your overrides in /etc/fail2ban/jail.local:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Or use the recommended drop-in approach with a single focused file:
sudo nano /etc/fail2ban/jail.d/sshd.local
A complete, production-ready SSH jail:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
backend = systemd
maxretry = 4
findtime = 1h
bantime = 24h
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
Key parameters explained:
| Parameter | Value | Meaning |
|---|---|---|
maxretry | 4 | Failed attempts before ban |
findtime | 1h | Window for counting failures |
bantime | 24h | How long the IP stays banned |
ignoreip | CIDR list | IPs that are never banned |
backend | systemd | Read logs from journald |
Warning: Always add your own IP or management subnet to
ignoreipbefore enabling strict jails. Getting locked out of a cloud VM requires console access to recover.
After any configuration change, reload Fail2Ban:
sudo fail2ban-client reload
Progressive Banning
Standard bans release the attacker after bantime expires. Progressive banning increases the ban duration exponentially for repeat offenders, making persistent attackers effectively permanent.
Add these settings to [DEFAULT] in jail.local:
[DEFAULT]
bantime.increment = true
bantime.factor = 1
bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
bantime.multipliers = 1 5 30 60 300 720 1440 2880
bantime.maxtime = 5w
bantime.overalljails = true
With the multiplier sequence 1 5 30 60 300 720 1440 2880 and a base bantime of 10 minutes:
- 1st ban: 10 minutes
- 2nd ban: 50 minutes
- 3rd ban: 5 hours
- 4th ban: 10 hours
- 5th ban: ~2 days
- …up to 5 weeks maximum
bantime.overalljails = true counts failures across all jails, so an IP that triggers both the SSH and Nginx jails accumulates bans faster.
Custom Filter Creation
When you run a non-standard application, you need a custom filter. Filters live in /etc/fail2ban/filter.d/.
Example: protecting a Node.js Express API that logs:
2026-03-22 14:05:33 [AUTH FAIL] 203.0.113.42 - Invalid token on /api/v1/login
Create /etc/fail2ban/filter.d/myapp-auth.conf:
[Definition]
failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[AUTH FAIL\] <HOST> -
ignoreregex =
<HOST> is Fail2Ban’s magic token that captures the IPv4 or IPv6 address.
Create the jail in /etc/fail2ban/jail.d/myapp.local:
[myapp-auth]
enabled = true
filter = myapp-auth
logpath = /var/log/myapp/app.log
maxretry = 5
findtime = 10m
bantime = 1h
port = 443,8443
Test your regex against the log file before activating:
sudo fail2ban-regex /var/log/myapp/app.log /etc/fail2ban/filter.d/myapp-auth.conf
Ideal output shows matched lines and zero false positives in the Missed section.
Nginx Protection
Fail2Ban ships with several Nginx filters. Enable them by adding to jail.local or a drop-in:
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 1h
[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 24h
findtime = 1h
[nginx-limit-req]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
bantime = 10m
nginx-http-auth catches failed Basic Auth attempts. nginx-botsearch blocks IPs scanning for WordPress, phpMyAdmin, and similar endpoints. nginx-limit-req bans IPs triggering your limit_req_zone rate limit.
Apache and Mail Server Jails
Apache
[apache-auth]
enabled = true
port = http,https
logpath = /var/log/apache2/error.log
maxretry = 3
[apache-badbots]
enabled = true
port = http,https
logpath = /var/log/apache2/access.log
bantime = 48h
Postfix / Dovecot (Mail Server)
[postfix]
enabled = true
port = smtp,465,submission
logpath = /var/log/mail.log
maxretry = 3
[postfix-sasl]
enabled = true
port = smtp,465,submission
logpath = /var/log/mail.log
maxretry = 3
[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps,submission,465,sieve
logpath = /var/log/mail.log
maxretry = 3
bantime = 12h
Mail servers are some of the most aggressively targeted services on the internet. Set maxretry conservatively (3) and bantime generously (12–24h) for mail jails.
Managing Bans with fail2ban-client
fail2ban-client is the primary administrative interface:
# Show all active jails
sudo fail2ban-client status
# Show banned IPs in a specific jail
sudo fail2ban-client status sshd
# Manually ban an IP
sudo fail2ban-client set sshd banip 203.0.113.42
# Unban an IP
sudo fail2ban-client set sshd unbanip 203.0.113.42
# Reload configuration without restart
sudo fail2ban-client reload
# Flush all bans in a jail
sudo fail2ban-client set sshd unbanip --all
To see all currently banned IPs across all jails:
sudo fail2ban-client banned
To inspect the ban database directly:
sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 \
"SELECT ip, bantime, jail FROM bans ORDER BY timeofban DESC LIMIT 20;"
Action Configuration
Actions define how Fail2Ban bans an IP. Configure the default action in [DEFAULT]:
[DEFAULT]
# iptables (classic, most compatible)
banaction = iptables-multiport
# nftables (recommended for modern distros)
banaction = nftables-multiport
# firewalld (RHEL/Rocky)
banaction = firewallcmd-ipset
# Send email notification on ban
action = %(action_mwl)s
The %(action_mwl)s action bans the IP and sends an email with the ban reason and relevant log lines. Configure the mail destination:
[DEFAULT]
destemail = admin@yourdomain.com
sendername = Fail2Ban
mta = sendmail
For notifications without banning via a custom action, combine actions:
action = %(banaction)s[name=%(__name__)s, port="%(port)s"]
%(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s"]
Testing Filters with fail2ban-regex
Before deploying any filter, validate it against real log data:
# Test against a log file
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# Test against a log file with a date pattern override
sudo fail2ban-regex /var/log/auth.log sshd --print-all-matched
# Test with a single log line
sudo fail2ban-regex "Mar 22 14:05:33 server sshd[12345]: Failed password for root from 203.0.113.42 port 54321 ssh2" sshd
A well-tuned filter shows:
Results
=======
Failregex: 47 total
...
Miss: 0 total
Ignore: 3 total (whitelisted IPs)
Aim for zero misses and investigate any unexpectedly high “Ignored” counts.
Cloudflare Integration
If your server sits behind Cloudflare, attacker IPs in your logs are Cloudflare proxy IPs — useless for banning at the server firewall. Two approaches solve this:
Option 1 — Restore real visitor IPs via the CF-Connecting-IP header. Configure Nginx to use it as the client address:
# /etc/nginx/conf.d/cloudflare-real-ip.conf
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
# (add all Cloudflare IP ranges from https://www.cloudflare.com/ips/)
real_ip_header CF-Connecting-IP;
This lets Fail2Ban see and ban the real attacker IPs at the server level.
Option 2 — Ban at the Cloudflare edge using the cloudflare action. Install the action:
sudo apt install fail2ban-doc # contains cloudflare action on some distros
# Or manually create /etc/fail2ban/action.d/cloudflare.conf
Configure credentials in jail.local:
[DEFAULT]
cftoken = YOUR_CLOUDFLARE_API_TOKEN
cfzoneid = YOUR_ZONE_ID
[nginx-botsearch]
enabled = true
action = %(action_)s
cloudflare[cftoken="%(cftoken)s", cfzoneid="%(cfzoneid)s"]
This creates a Cloudflare firewall rule blocking the IP at the CDN level — before traffic ever reaches your server.
Fail2Ban vs Alternatives
| Tool | Mechanism | Language | Scope | Crowdsourced |
|---|---|---|---|---|
| Fail2Ban | Log regex + local firewall | Python | Local server | No |
| CrowdSec | Behavioral analysis + shared blocklists | Go | Multi-server | Yes |
| DenyHosts | SSH /etc/hosts.deny only | Python | SSH only | Optional |
| SSHGuard | C daemon, multi-backend | C | Limited services | No |
Choose Fail2Ban when you need a battle-tested, single-server solution with broad service support and flexible regex-based matching.
Choose CrowdSec when you operate multiple servers and want community threat intelligence and a richer behavioral engine.
Avoid DenyHosts for new deployments — it is no longer actively maintained and only protects SSH via the deprecated hosts.deny mechanism.
Production Hardening Tips
- Change the SSH port from 22 to something non-standard. Fail2Ban still works — just update the
portdirective. This alone eliminates most automated scanning noise. - Set
bantimeto at least 24h for SSH. Ten-minute bans barely slow down determined attackers. - Monitor the Fail2Ban log at
/var/log/fail2ban.logwith a log aggregator or simple cron alerts. - Back up
/var/lib/fail2ban/fail2ban.sqlite3to preserve ban history across rebuilds. - Use
bantime.overalljails = trueso attackers accumulate bans from all jails combined. - Test filters before enabling jails in production with
fail2ban-regexand real log samples. - Combine with UFW rate limiting (
sudo ufw limit ssh) for defense in depth — UFW drops obvious port scanners before they generate enough log lines for Fail2Ban. - Audit your ignoreip list periodically. Forgotten whitelisted IPs from old VPNs or office ranges can create blind spots.
Summary
Fail2Ban provides robust, automated brute-force protection for any Linux server exposing services to the internet. Key takeaways:
- Fail2Ban monitors logs, matches failure patterns with regex, and adds firewall rules to block offending IPs
- Never edit
jail.conf— usejail.localor drop-in files injail.d/ - Always configure
ignoreipwith your own IP before enabling strict jails - Progressive banning (
bantime.increment) makes persistent attackers effectively permanent - Custom filters let you protect any application that logs authentication failures
- Use
fail2ban-regexto test and validate filter patterns before deployment - For Cloudflare-proxied sites, restore real IPs via
CF-Connecting-IPor ban at the Cloudflare edge