Every server exposed to the internet is under constant attack. Automated bots scan SSH ports, brute-force login pages, and probe web applications within minutes of a new IP going online. Fail2Ban is the standard defense: it monitors log files for malicious patterns and automatically bans offending IP addresses by adding firewall rules. A default Fail2Ban installation helps, but advanced configuration transforms it from a basic SSH protector into a comprehensive intrusion prevention system covering SSH, Nginx, Apache, WordPress, and custom applications.
This guide goes deep: understanding Fail2Ban's architecture, configuring jails for every common service, writing custom filters with regex, integrating with UFW, setting up email and Slack notifications, creating Cloudflare ban actions, and monitoring ban statistics. By the end, your server will automatically detect and block attackers across all your services.
Prerequisites
Before starting, you need:
- An Ubuntu 24.04 VPS. A Cloud VPS from MassiveGRID with any configuration will work — Fail2Ban uses minimal resources.
- Root or sudo access. If you haven't set up your server yet, follow our Ubuntu VPS setup guide and security hardening guide first.
- UFW firewall enabled. Fail2Ban will integrate with UFW for banning. Confirm with
sudo ufw status. - Services running. You'll configure jails for the services you actually run (SSH, Nginx, Apache, etc.).
Why not just use UFW alone? UFW is a static firewall — it blocks or allows based on rules you write manually. Fail2Ban is dynamic — it reads log files in real-time, detects attack patterns, and creates temporary firewall rules automatically. The two work together: UFW provides the baseline rules, and Fail2Ban adds reactive, time-limited bans for detected threats.
MassiveGRID Ubuntu VPS Includes
Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
→ Deploy a self-managed VPS — from $1.99/mo
→ Need dedicated resources? — from $19.80/mo
→ Want fully managed hosting? — we handle everything
Fail2Ban Architecture
Understanding how Fail2Ban works internally helps you configure it correctly and debug issues.
Core Components
- Jails: The top-level configuration unit. Each jail monitors a specific log file for a specific type of attack. A jail combines a filter, one or more actions, and timing parameters (ban time, find time, max retries).
- Filters: Regular expressions that define what a "failed" event looks like in a log file. Fail2Ban ships with pre-built filters for dozens of services. You can also write custom filters.
- Actions: What happens when an IP reaches the max retry threshold. The default action adds a firewall rule to block the IP. Other actions include sending email, calling webhooks, or interacting with APIs like Cloudflare.
- fail2ban-server: The daemon that runs in the background, reads logs, applies filters, and executes actions.
- fail2ban-client: The CLI tool for interacting with the running server (checking status, banning/unbanning IPs).
Configuration File Hierarchy
/etc/fail2ban/
├── fail2ban.conf # Main daemon configuration (don't edit)
├── fail2ban.local # Your overrides for fail2ban.conf
├── jail.conf # Default jail definitions (don't edit)
├── jail.local # Your jail overrides and custom jails
├── jail.d/ # Additional jail configuration files
├── filter.d/ # Filter definitions (regex patterns)
│ ├── sshd.conf # SSH filter (built-in)
│ ├── nginx-http-auth.conf # Nginx auth filter (built-in)
│ └── ... # Many more built-in filters
├── action.d/ # Action definitions
│ ├── ufw.conf # UFW action (built-in)
│ ├── sendmail.conf # Email action (built-in)
│ └── ... # Many more built-in actions
└── paths-*.conf # Log path definitions per distribution
Critical rule: Never edit jail.conf, fail2ban.conf, or any file in filter.d/ directly. Package updates will overwrite them. Instead, create .local files that override specific settings. Fail2Ban reads .conf first, then applies .local on top.
Installing and Initial Configuration
Install Fail2Ban:
sudo apt update
sudo apt install -y fail2ban
Fail2Ban starts automatically. Verify:
sudo systemctl status fail2ban
Create the local configuration file:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
However, starting from scratch with only the settings you need is cleaner. Create a minimal jail.local:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# Ban duration (1 hour)
bantime = 3600
# Window to count failures (10 minutes)
findtime = 600
# Number of failures before ban
maxretry = 5
# Action to take: ban via UFW and send notification
banaction = ufw
banaction_allports = ufw
# Ignore localhost and your own IPs
ignoreip = 127.0.0.1/8 ::1
# Backend for log monitoring
backend = systemd
# --- JAILS ---
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 3600
findtime = 600
Restart Fail2Ban to apply:
sudo systemctl restart fail2ban
Verify the SSH jail is active:
sudo fail2ban-client status
sudo fail2ban-client status sshd
Configuring Jails for Common Services
SSH Jail (Enhanced)
The default SSH jail is good, but for production we want stricter settings and progressive banning:
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
findtime = 600
bantime = 3600
# Ban for longer on repeated offenses
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
# Use aggressive mode to catch more attack patterns
mode = aggressive
The bantime.increment feature doubles the ban time for repeat offenders. First ban: 1 hour. Second ban: 2 hours. Third: 4 hours. Up to a maximum of 7 days (604800 seconds).
Nginx HTTP Authentication Jail
If you use Nginx basic authentication for admin areas:
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 600
bantime = 3600
Nginx Bad Bots and Scanners
Block bots that request suspicious URLs (phpMyAdmin probes, .env file requests, wp-login attacks on non-WordPress servers):
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
findtime = 600
bantime = 86400
Nginx Rate Limit Jail
If you've configured rate limiting in Nginx (as described in our Nginx reverse proxy guide), ban IPs that repeatedly hit the rate limit:
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 600
bantime = 3600
Apache Jails
For servers running Apache instead of Nginx:
[apache-auth]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 5
findtime = 600
bantime = 3600
[apache-badbots]
enabled = true
port = http,https
filter = apache-badbots
logpath = /var/log/apache2/access.log
maxretry = 2
findtime = 600
bantime = 86400
[apache-overflows]
enabled = true
port = http,https
filter = apache-overflows
logpath = /var/log/apache2/error.log
maxretry = 2
findtime = 600
bantime = 3600
WordPress Login Protection
Create a custom filter for WordPress login failures. First, create the filter:
sudo nano /etc/fail2ban/filter.d/wordpress-login.local
[Definition]
failregex = ^ .* "POST /wp-login\.php
^ .* "POST /xmlrpc\.php
datepattern = ^%%d/%%b/%%Y:%%H:%%M:%%S %%z
ignoreregex =
Add the jail:
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600
MariaDB/MySQL Jail
Protect against brute-force database login attempts (relevant if you allow remote database access as described in our MariaDB installation guide):
[mysqld-auth]
enabled = true
port = 3306
filter = mysqld-auth
logpath = /var/log/mysql/error.log
maxretry = 5
findtime = 600
bantime = 3600
Creating Custom Filters with Regex
Fail2Ban's power comes from regex filters. Each filter defines a failregex that matches a failed authentication or malicious request in a log file. The <HOST> placeholder captures the IP address.
Understanding the Filter Format
A filter file has this structure:
[Definition]
failregex = placeholder>
ignoreregex =
The <HOST> tag is replaced with a regex that matches IPv4 and IPv6 addresses. Everything else in the pattern must match the actual log line.
Custom Filter: Repeated 404 Errors
Block scanners that generate many 404 errors probing for vulnerabilities:
sudo nano /etc/fail2ban/filter.d/nginx-404.local
[Definition]
failregex = ^ .* "(GET|POST|HEAD) .* HTTP/[12]\.[01]" 404
ignoreregex = \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$
The ignoreregex excludes legitimate 404s for missing static assets.
Add the jail in jail.local:
[nginx-404]
enabled = true
port = http,https
filter = nginx-404
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 600
bantime = 3600
Custom Filter: Blocking Specific User-Agents
sudo nano /etc/fail2ban/filter.d/nginx-bad-useragent.local
[Definition]
failregex = ^ .* "(GET|POST|HEAD) .* HTTP/[12]\.[01]" .* "(sqlmap|nikto|nessus|masscan|zgrab|python-requests/2|Go-http-client|curl/[0-9])"
ignoreregex =
[nginx-bad-useragent]
enabled = true
port = http,https
filter = nginx-bad-useragent
logpath = /var/log/nginx/access.log
maxretry = 1
findtime = 86400
bantime = 604800
Testing Custom Filters
Before activating a custom filter, test it against your actual log file:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-404.local
This shows how many lines match, how many IPs would be captured, and whether the regex works correctly. Always test before deploying.
# Test with a single line
echo '192.168.1.100 - - [27/Feb/2026:10:15:30 +0000] "GET /wp-admin HTTP/1.1" 404 162' | sudo fail2ban-regex - /etc/fail2ban/filter.d/nginx-404.local
Ban Times, Find Times, and Max Retries
The three timing parameters control how aggressive Fail2Ban is:
| Parameter | Description | Recommended Range |
|---|---|---|
maxretry |
Number of failures before ban | 3-5 for auth, 10-20 for HTTP |
findtime |
Time window to count failures | 300-600 seconds |
bantime |
How long the IP is banned | 3600-86400 seconds |
Progressive Banning
Enable progressive banning in the [DEFAULT] section to automatically increase ban times for repeat offenders:
[DEFAULT]
bantime = 3600
bantime.increment = true
bantime.factor = 2
bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
bantime.maxtime = 604800
With this configuration:
- First offense: 1 hour ban
- Second offense: ~2 hours
- Third offense: ~5.4 hours
- Fourth offense: ~14.8 hours
- Fifth offense: ~2.7 days
- Maximum: 7 days
Permanent Bans
To permanently ban an IP (use with caution):
bantime = -1
A bantime of -1 means the ban never expires. Only use this for jails that catch genuinely malicious behavior (like vulnerability scanners) where false positives are extremely unlikely.
IP Whitelisting
Whitelisting ensures you never accidentally ban yourself or trusted IPs.
Global Whitelist
In the [DEFAULT] section of jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 198.51.100.50 10.0.0.0/8
This whitelists:
127.0.0.1/8— localhost::1— IPv6 localhost198.51.100.50— your office IP (replace with your actual IP)10.0.0.0/8— internal network range
Per-Jail Whitelist
Override the whitelist for a specific jail:
[sshd]
enabled = true
ignoreip = 127.0.0.1/8 ::1 198.51.100.50 198.51.100.51
Dynamic Whitelist via Command
Temporarily whitelist an IP without editing configuration:
sudo fail2ban-client set sshd addignoreip 198.51.100.60
This lasts until Fail2Ban is restarted.
Email Notifications for Bans
Get notified when Fail2Ban bans an IP. This helps you stay aware of attack patterns and identify any false positives.
Install Mail Utilities
sudo apt install -y mailutils
If you need a working SMTP relay, configure Postfix as a satellite system or use an external SMTP service.
Configure Email Action
In jail.local, set the default action to include email:
[DEFAULT]
destemail = admin@yourdomain.com
sender = fail2ban@yourdomain.com
mta = mail
# Action with email notification and whois lookup
action = %(action_mwl)s
The action shortcuts:
%(action_)s— ban only (default)%(action_mw)s— ban + email with whois info%(action_mwl)s— ban + email with whois + relevant log lines
The action_mwl action sends an email containing the banned IP, whois information, and the log lines that triggered the ban — extremely useful for post-incident analysis.
Test Email Notifications
Trigger a test by banning a test IP:
sudo fail2ban-client set sshd banip 192.0.2.1
Check your email for the notification, then unban:
sudo fail2ban-client set sshd unbanip 192.0.2.1
Fail2Ban with UFW Integration
By default on Ubuntu, Fail2Ban uses iptables directly. For consistency with your existing UFW rules, configure Fail2Ban to use UFW as its ban action.
Set UFW as the Default Action
In the [DEFAULT] section of jail.local:
[DEFAULT]
banaction = ufw
banaction_allports = ufw
This tells Fail2Ban to use /etc/fail2ban/action.d/ufw.conf for banning, which inserts UFW rules dynamically.
Verify the Integration
After Fail2Ban bans an IP, you can see the ban in both systems:
# Check Fail2Ban
sudo fail2ban-client status sshd
# Check UFW (banned IPs appear as DENY rules)
sudo ufw status numbered
When the ban expires, Fail2Ban automatically removes the UFW rule.
Alternative: iptables-multiport
If you prefer Fail2Ban to manage bans independently of UFW (to avoid cluttering your UFW rule list):
[DEFAULT]
banaction = iptables-multiport
banaction_allports = iptables-allports
Both approaches work. UFW integration keeps everything visible in one place. iptables gives Fail2Ban its own chain that's invisible to ufw status but visible with iptables -L.
Monitoring Banned IPs and Statistics
Check Overall Status
sudo fail2ban-client status
This lists all active jails.
Check a Specific Jail
sudo fail2ban-client status sshd
Output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 3
| |- Total failed: 847
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 12
|- Total banned: 156
`- Banned IP list: 192.0.2.1 192.0.2.2 ...
View All Currently Banned IPs Across All Jails
sudo fail2ban-client banned
Check If a Specific IP Is Banned
sudo fail2ban-client get sshd banip --with-time | grep "198.51.100.100"
View the Fail2Ban Log
sudo tail -100 /var/log/fail2ban.log
The log shows every ban and unban event with timestamps:
2026-02-27 10:15:30,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.0.2.50
2026-02-27 11:15:30,456 fail2ban.actions [1234]: NOTICE [sshd] Unban 192.0.2.50
Ban Statistics Script
Create a script to show daily ban statistics:
sudo nano /usr/local/bin/fail2ban-stats.sh
#!/bin/bash
# Fail2Ban statistics summary
echo "=== Fail2Ban Statistics ==="
echo ""
echo "-- Active Jails --"
sudo fail2ban-client status | grep "Jail list" | sed 's/.*:\s*//' | tr ',' '\n' | sed 's/^\s*/ /'
echo ""
echo "-- Current Bans Per Jail --"
for jail in $(sudo fail2ban-client status | grep "Jail list" | sed 's/.*:\s*//' | tr ',' ' '); do
jail=$(echo $jail | tr -d ' ')
count=$(sudo fail2ban-client status $jail | grep "Currently banned" | awk '{print $NF}')
total=$(sudo fail2ban-client status $jail | grep "Total banned" | awk '{print $NF}')
echo " $jail: $count currently banned, $total total"
done
echo ""
echo "-- Today's Bans --"
today=$(date +%Y-%m-%d)
bans=$(grep "$today" /var/log/fail2ban.log 2>/dev/null | grep -c "Ban ")
unbans=$(grep "$today" /var/log/fail2ban.log 2>/dev/null | grep -c "Unban ")
echo " Bans: $bans"
echo " Unbans: $unbans"
echo ""
echo "-- Top 10 Banned IPs (All Time) --"
grep "Ban " /var/log/fail2ban.log 2>/dev/null | awk '{print $NF}' | sort | uniq -c | sort -rn | head -10 | while read count ip; do
echo " $ip: $count bans"
done
sudo chmod +x /usr/local/bin/fail2ban-stats.sh
Run it anytime:
sudo /usr/local/bin/fail2ban-stats.sh
Advanced: Custom Actions
Cloudflare Firewall Action
If your server sits behind Cloudflare, banning at the firewall level doesn't help — the attacker's real IP is in the X-Forwarded-For header, and the connection comes from Cloudflare's IPs. Instead, ban the IP at Cloudflare's edge.
Create the Cloudflare action:
sudo nano /etc/fail2ban/action.d/cloudflare-ban.local
[Definition]
actionban = curl -s -o /dev/null -X POST \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"mode":"block","configuration":{"target":"ip","value":""},"notes":"Fail2Ban "}' \
"https://api.cloudflare.com/client/v4/accounts//firewall/access_rules/rules"
actionunban = RULEID=$(curl -s -X GET \
-H "Authorization: Bearer " \
"https://api.cloudflare.com/client/v4/accounts//firewall/access_rules/rules?configuration.value=" \
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['result'][0]['id'] if r['result'] else '')") && \
[ -n "$RULEID" ] && curl -s -o /dev/null -X DELETE \
-H "Authorization: Bearer " \
"https://api.cloudflare.com/client/v4/accounts//firewall/access_rules/rules/$RULEID"
[Init]
cftoken = your-cloudflare-api-token
cfaccountid = your-cloudflare-account-id
Use it in a jail:
[nginx-cloudflare]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 86400
action = cloudflare-ban
Slack Notification Action
Send ban alerts to a Slack channel:
sudo nano /etc/fail2ban/action.d/slack-notify.local
[Definition]
actionban = curl -s -o /dev/null -X POST \
-H "Content-type: application/json" \
--data '{"text":":no_entry: *Fail2Ban* banned `` in jail `` for s (failures: )"}' \
actionunban = curl -s -o /dev/null -X POST \
-H "Content-type: application/json" \
--data '{"text":":white_check_mark: *Fail2Ban* unbanned `` from jail ``"}' \
[Init]
slack_webhook = https://hooks.slack.com/services/YOUR/WEBHOOK/URL
Combine the Slack notification with the UFW ban action:
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 3600
action = ufw
slack-notify
This bans the IP via UFW and sends a Slack notification. Multiple actions are specified on separate indented lines.
Custom Action: Log to a File
If you want a simple, separate log of all ban events for external processing:
sudo nano /etc/fail2ban/action.d/log-bans.local
[Definition]
actionban = echo "$(date '+%%Y-%%m-%%d %%H:%%M:%%S') BAN jail= failures=" >> /var/log/fail2ban-bans.log
actionunban = echo "$(date '+%%Y-%%m-%%d %%H:%%M:%%S') UNBAN jail=" >> /var/log/fail2ban-bans.log
Unbanning IPs and Managing the Ban List
Unban a Specific IP from a Specific Jail
sudo fail2ban-client set sshd unbanip 192.0.2.50
Unban an IP from All Jails
sudo fail2ban-client unban 192.0.2.50
Unban All IPs from All Jails
sudo fail2ban-client unban --all
Manually Ban an IP
sudo fail2ban-client set sshd banip 192.0.2.100
Reload Configuration Without Restarting
After editing jail.local or filter files:
sudo fail2ban-client reload
Reload a specific jail:
sudo fail2ban-client reload sshd
Complete jail.local Example
Here's a comprehensive jail.local for a server running SSH, Nginx, and a web application:
[DEFAULT]
# Default ban settings
bantime = 3600
findtime = 600
maxretry = 5
# Progressive banning
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
# Use UFW for banning
banaction = ufw
banaction_allports = ufw
# Whitelist
ignoreip = 127.0.0.1/8 ::1
# Email notifications
destemail = admin@yourdomain.com
sender = fail2ban@yourdomain.com
mta = mail
action = %(action_mwl)s
# Backend
backend = auto
# --- JAILS ---
[sshd]
enabled = true
port = ssh
filter = sshd
mode = aggressive
maxretry = 3
findtime = 600
bantime = 3600
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 600
bantime = 3600
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
findtime = 600
bantime = 86400
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 600
bantime = 3600
[nginx-404]
enabled = true
port = http,https
filter = nginx-404
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 600
bantime = 3600
Troubleshooting
Fail2Ban Won't Start
Check the configuration for syntax errors:
sudo fail2ban-client -t
Check the log for errors:
sudo journalctl -u fail2ban -n 50
Filter Isn't Matching
Test the filter against the log file:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-404.local --print-all-matched
Common issues: wrong log path, log format doesn't match the regex, or the datepattern doesn't match the log's timestamp format.
Bans Aren't Taking Effect
Verify the action is working:
# Check iptables rules
sudo iptables -L -n | grep f2b
# Check UFW rules
sudo ufw status numbered
# Check if the ban was registered
sudo fail2ban-client get sshd banip
Too Many False Positives
If legitimate users are getting banned:
- Increase
maxretry - Decrease
findtime(shorter window means the user needs to fail rapidly) - Add their IP to
ignoreip - Review the filter regex for overly broad patterns
Fail2Ban Using Too Much Memory
If monitoring large log files, Fail2Ban can consume significant memory. Mitigate by:
- Ensuring log rotation is active (large log files cause high memory usage)
- Reducing
findtime(less history to track) - Using
backend = systemdfor services that log to the journal
Prefer Managed Security?
If you'd rather not manage intrusion prevention, firewall rules, log monitoring, and security notifications yourself, consider MassiveGRID's Managed Dedicated Cloud Servers. The managed service handles Fail2Ban configuration, firewall management, intrusion detection, and 24/7 security monitoring — so your infrastructure stays protected without the operational overhead. Every managed server includes 12 Tbps DDoS protection at the network edge and proactive incident response from MassiveGRID's security team.
What's Next
- Ubuntu VPS initial setup guide — foundational server configuration
- Security hardening guide — comprehensive hardening beyond Fail2Ban
- Network configuration and troubleshooting — UFW advanced rules and network diagnostics
- Automated security updates — keep your server patched automatically
- VPS monitoring setup — dashboards and alerts for security events
- WireGuard VPN setup — secure access to admin ports via encrypted tunnel