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:

  1. Log monitoring — Fail2Ban’s logtarget daemon watches log files (e.g., /var/log/auth.log, /var/log/nginx/error.log) using inotify or polling.
  2. Regex matching — Each jail references a filter containing one or more failregex patterns. When a log line matches, Fail2Ban increments a failure counter for the source IP.
  3. Threshold check — Once the counter reaches maxretry within the findtime window, the IP is considered hostile.
  4. 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 bantime seconds.

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:

ParameterValueMeaning
maxretry4Failed attempts before ban
findtime1hWindow for counting failures
bantime24hHow long the IP stays banned
ignoreipCIDR listIPs that are never banned
backendsystemdRead logs from journald

Warning: Always add your own IP or management subnet to ignoreip before 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

ToolMechanismLanguageScopeCrowdsourced
Fail2BanLog regex + local firewallPythonLocal serverNo
CrowdSecBehavioral analysis + shared blocklistsGoMulti-serverYes
DenyHostsSSH /etc/hosts.deny onlyPythonSSH onlyOptional
SSHGuardC daemon, multi-backendCLimited servicesNo

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 port directive. This alone eliminates most automated scanning noise.
  • Set bantime to at least 24h for SSH. Ten-minute bans barely slow down determined attackers.
  • Monitor the Fail2Ban log at /var/log/fail2ban.log with a log aggregator or simple cron alerts.
  • Back up /var/lib/fail2ban/fail2ban.sqlite3 to preserve ban history across rebuilds.
  • Use bantime.overalljails = true so attackers accumulate bans from all jails combined.
  • Test filters before enabling jails in production with fail2ban-regex and 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 — use jail.local or drop-in files in jail.d/
  • Always configure ignoreip with 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-regex to test and validate filter patterns before deployment
  • For Cloudflare-proxied sites, restore real IPs via CF-Connecting-IP or ban at the Cloudflare edge