Security

Advanced Fail2Ban Configuration: Custom Filters, Recidive Jails, and Incident Response

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

Introduction: Why Every Public Server Is Under Attack Right Now

Within minutes of spinning up a new VPS, your SSH port is being probed. It is not hypothetical — it is measurable. Automated scanners sweep the entire IPv4 address space continuously, testing thousands of username/password combinations per minute against every server they find.

Brute force attacks are not sophisticated. They are persistent, automated, and cheap to run. A single compromised server can launch millions of login attempts per day. Without active defense, your server is absorbing this noise 24/7, consuming CPU cycles, filling logs, and exposing you to eventual compromise when a weak password or a known default credential is discovered.

Fail2Ban is the industry-standard first line of defense against this class of attack. It is not a firewall replacement, not a WAF, and not a substitute for SSH key authentication. It is a log-monitoring daemon that watches your logs in real time, identifies repeated failure patterns, and instructs your firewall to ban the offending IP for a configurable period.

In this masterclass, you will learn how Fail2Ban works from first principles, how to configure it for SSH, Nginx, and WordPress, how to write your own filters, and how to build a production-hardened configuration that protects your servers around the clock.

If you just need a working SSH jail in the next five minutes, start with our Fail2Ban Setup Guide instead. This guide picks up where that one stops — custom filters, progressive bans, recidive jails, and the mistakes that get admins locked out of their own servers.


1. How Fail2Ban Works: The Architecture

Fail2Ban's architecture is deceptively simple, which is exactly why it has remained the standard tool for this job for over two decades.

The core processing flow looks like this:

Log File (auth.log, nginx/access.log, etc.)
    │
    ▼
Filter (regex patterns defined per service)
    │   → Does this line match a failure pattern?
    ▼
Jail (maxretry + findtime window)
    │   → Has this IP failed N times in T seconds?
    ▼
Action (iptables / nftables / cloudflare / email)
    │   → Ban IP for bantime seconds
    ▼
Unban (after bantime expires or manual override)

Components Explained

  • Filters — Regex definitions stored in /etc/fail2ban/filter.d/. Each filter targets one service's log format and extracts the <HOST> (the attacking IP) from failure lines.
  • Jails — Configured in /etc/fail2ban/jail.local. A jail binds a filter to a log file and defines thresholds: how many failures (maxretry), in what time window (findtime), and how long to ban (bantime).
  • Actions — What happens when a ban triggers. The default action calls iptables or nftables to add a DROP rule for the offending IP. Actions can also send email alerts or call external APIs (e.g., Cloudflare).

The Database

Fail2Ban maintains a SQLite database (default: /var/lib/fail2ban/fail2ban.sqlite3) that persists ban information across restarts. Without this, a server reboot would clear all active bans. Configure the database purge age with dbpurgeage (default: 86400 seconds = 1 day).

Backend: iptables vs nftables

On Ubuntu 22.04 and later, the default firewall backend is nftables. Fail2Ban ships with actions for both. If your system uses nftables (check with nft list ruleset), use banaction = nftables-multiport in your configuration. On older systems using iptables, use banaction = iptables-multiport.


2. Installation and Basic Setup

Installing Fail2Ban

apt update
apt install fail2ban -y

Fail2Ban is available in the default Ubuntu/Debian repositories. No PPAs or external repositories required.

The Golden Rule: Never Edit jail.conf

The file /etc/fail2ban/jail.conf is the default configuration file. It is overwritten on every package upgrade. Any changes you make there will be lost.

Always configure Fail2Ban in /etc/fail2ban/jail.local. This file overrides jail.conf and is preserved across upgrades. If it does not exist, create it:

touch /etc/fail2ban/jail.local

Basic Default Configuration

Start with sensible defaults in the [DEFAULT] section, which applies to all jails unless overridden:

[DEFAULT]
# Ban duration in seconds (3600 = 1 hour)
bantime = 3600

# Time window to count failures (600 = 10 minutes)
findtime = 600

# Number of failures before ban
maxretry = 5

# Use nftables on Ubuntu 22.04+, iptables on older systems
banaction = nftables-multiport

# Keep ban data across restarts (86400 = 24 hours)
dbpurgeage = 86400

# Your IP(s) — NEVER ban yourself
ignoreip = 127.0.0.1/8 ::1

Enable and Start Fail2Ban

systemctl enable --now fail2ban
systemctl status fail2ban

Verify it is running and check the initial log:

tail -f /var/log/fail2ban.log

3. Protecting SSH: The Number-One Priority

SSH is the primary attack surface on any public server. Dictionary attacks against root, admin, ubuntu, and common usernames begin within minutes of server provisioning. The built-in sshd jail handles this, but the defaults are too permissive for production use.

Understanding the Default sshd Jail

Fail2Ban ships with a pre-configured sshd jail in jail.conf. On Ubuntu, it is enabled by default (controlled by the fail2ban-defaults.conf package defaults). The filter at /etc/fail2ban/filter.d/sshd.conf matches patterns in /var/log/auth.log.

Production SSH Jail Configuration

Override the defaults in jail.local with production-hardened values:

[sshd]
enabled = true

# Port(s) SSH is listening on — list custom ports if changed
port = ssh,2222

# Filter to use (matches /etc/fail2ban/filter.d/sshd.conf)
filter = sshd

# Log file to monitor
logpath = /var/log/auth.log

# More aggressive: 3 failures triggers ban
maxretry = 3

# Ban for 24 hours
bantime = 86400

# Count failures within 5 minutes
findtime = 300

# Aggressive mode catches more patterns (connection resets, etc.)
mode = aggressive

Aggressive Mode

The mode = aggressive directive enables additional regex patterns in the sshd filter. Standard mode catches failed password attempts and invalid usernames. Aggressive mode also catches connection resets, pre-auth disconnects, and probing behavior that precedes actual brute force attempts.

Testing Your SSH Filter

Before deploying a filter, test it against your actual log file to verify it matches:

fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

This prints statistics on how many lines matched. You should see matches for recent failed login attempts.

Why SSH Key Authentication + Fail2Ban Is the Right Combination

Fail2Ban is not a substitute for SSH key authentication — it is a complement. With SSH keys, password authentication is disabled entirely, and brute force attacks become meaningless. But scanning bots still generate log noise and consume server resources. Fail2Ban eliminates the scanners at the firewall level, keeping your logs clean and reducing attack surface further.

The correct hardening order is: disable password auth → deploy SSH keys → configure Fail2Ban to catch remaining probes and pre-auth attacks.


4. Protecting Nginx

Web servers present a different attack surface than SSH. Attackers probe for admin panels, test credentials against login endpoints, scan for vulnerable PHP files, and run automated vulnerability scanners. Fail2Ban's Nginx jails handle all of these patterns.

4a. Nginx HTTP Authentication

If you use Nginx's built-in HTTP Basic Authentication (protected directories, staging sites), the nginx-http-auth filter handles failed attempts:

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600

The filter matches lines like no user/password was provided for basic authentication and user "admin" was not found in the Nginx error log.

4b. Nginx 4xx Response Filter (Custom)

Vulnerability scanners and automated bots generate large numbers of 404, 403, and 444 responses as they probe for common paths (/wp-admin, /.env, /phpmyadmin, etc.). Create a custom filter to catch this behavior:

Create /etc/fail2ban/filter.d/nginx-4xx.conf:

[Definition]
failregex = ^<HOST> .+ "(GET|POST|HEAD) .+" (403|404|444) \d+

ignoreregex = ^<HOST> .+ "(GET|POST) /favicon\.ico
              ^<HOST> .+ "(GET|POST) /robots\.txt

Then add the jail in jail.local:

[nginx-4xx]
enabled = true
filter = nginx-4xx
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 60
bantime = 3600

4c. Blocking Bad Bots and Aggressive Crawlers

Create /etc/fail2ban/filter.d/nginx-badbots.conf:

[Definition]
failregex = ^<HOST> -.* "(GET|POST|HEAD).+" \d+ \d+ ".*" "(Wget|curl|libwww-perl|python-requests|Go-http-client|nikto|sqlmap|masscan|nmap|zgrab|dirbuster|gobuster|wfuzz|nuclei).*"

ignoreregex =
[nginx-badbots]
enabled = true
filter = nginx-badbots
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 1
bantime = 86400
findtime = 60

Note maxretry = 1 — a single request from a known attack tool justifies an immediate 24-hour ban.

4d. Protecting Admin Login Pages

Create /etc/fail2ban/filter.d/nginx-login.conf to protect any login endpoint (Panelica, custom apps, admin panels):

[Definition]
failregex = ^<HOST> .+ "POST /(login|admin/login|wp-login\.php|user/login|auth/login).+ (401|403) \d+

ignoreregex =
[nginx-login]
enabled = true
filter = nginx-login
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 7200

5. Protecting WordPress

WordPress is the most attacked web application on the internet. Its predictable file structure (/wp-login.php, /xmlrpc.php, /wp-admin/) makes it a primary target for credential stuffing and brute force attacks. WordPress-specific Fail2Ban jails are essential for any server running WordPress sites.

5a. wp-login.php Brute Force

Create /etc/fail2ban/filter.d/wordpress-login.conf:

[Definition]
# Match POST requests to wp-login.php
failregex = ^<HOST> .+ "POST /wp-login\.php

ignoreregex =

Add the jail:

[wordpress-login]
enabled = true
filter = wordpress-login
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600

This catches every POST attempt to the WordPress login page. Five attempts in 5 minutes triggers a 1-hour ban.

5b. xmlrpc.php Attack Blocking

The WordPress XML-RPC API (/xmlrpc.php) is a legacy authentication interface that allows single requests to test thousands of username/password combinations via multicall. It is one of the most abused WordPress endpoints.

Unless you need XML-RPC for Jetpack or mobile apps, block it entirely at Nginx level. If you need it, add Fail2Ban protection:

Extend wordpress-login.conf to also match xmlrpc:

[Definition]
failregex = ^<HOST> .+ "POST /wp-login\.php
            ^<HOST> .+ "POST /xmlrpc\.php

ignoreregex =

Or block xmlrpc at Nginx entirely (recommended):

location = /xmlrpc.php {
    deny all;
    return 444;
}

5c. wp-admin Directory Protection

Protect the WordPress admin area from unauthorized access. Create /etc/fail2ban/filter.d/wordpress-admin.conf:

[Definition]
# Ban IPs generating 403 errors on wp-admin (not logged-in access attempts)
failregex = ^<HOST> .+ "GET /wp-admin/.+ 403

ignoreregex =
[wordpress-admin]
enabled = true
filter = wordpress-admin
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 600
bantime = 3600

Whitelist Your Own IP for WordPress

Always add your static IP to the ignoreip list in [DEFAULT] to avoid locking yourself out during development or testing:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

6. Writing Custom Filters: Step-by-Step

Fail2Ban's power is in its extensibility. If a service writes logs, you can protect it with Fail2Ban. Writing custom filters requires understanding the regex syntax and the <HOST> placeholder.

Regex Fundamentals for Fail2Ban

  • <HOST> — Mandatory placeholder that matches IPv4 and IPv6 addresses. Fail2Ban extracts this as the IP to ban.
  • Patterns are Python regex syntax (not POSIX or PCRE).
  • Each line in failregex is tested independently against each log line.
  • Lines matching ignoreregex are excluded from failure counting.

Step 1: Identify the Log Pattern

Find examples of failure lines in your log file:

grep "authentication failure" /var/log/auth.log | head -5
grep " 401 " /var/log/nginx/access.log | head -5
grep "Invalid user" /var/log/auth.log | head -5

Step 2: Write the Regex

A typical Nginx access log line looks like:

203.0.113.42 - - [28/Mar/2026:21:00:00 +0000] "POST /admin/login HTTP/1.1" 401 156 "-" "python-requests/2.28.0"

A regex that captures this:

^<HOST> .+ "POST /admin/login.+" 401 \d+

Step 3: Create the Filter File

Save your filter to /etc/fail2ban/filter.d/myapp-login.conf:

[INCLUDES]
before = common.conf

[Definition]
failregex = ^<HOST> .+ "POST /admin/login.+" (401|403) \d+

ignoreregex =

Step 4: Test the Filter

fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/myapp-login.conf

Output shows matched lines, unmatched lines, and extracted IPs. Iterate on your regex until you see the expected failures matched.

Step 5: Create the Jail and Reload

[myapp-login]
enabled = true
filter = myapp-login
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600
fail2ban-client reload

5 Production-Ready Custom Filter Examples

Filter 1: Generic 401 Unauthorized

failregex = ^<HOST> .+ ".*" 401 \d+

Filter 2: phpMyAdmin Login Attempts

failregex = ^<HOST> .+ "POST /phpmyadmin/index\.php.+" 200

Filter 3: Scanning for Hidden Files (.env, .git)

failregex = ^<HOST> .+ "GET /\.(env|git|htaccess|DS_Store|config).+" (200|403|404)

Filter 4: Vulnerability Scanner User-Agents

failregex = ^<HOST> .+ ".*" \d+ \d+ ".*" "(nikto|sqlmap|Acunetix|Nessus|OpenVAS|masscan|zgrab|nmap).*"

Filter 5: API Rate Abuse (429 responses)

failregex = ^<HOST> .+ ".*" 429 \d+

7. Actions: What Happens When an IP Gets Banned

Actions define what Fail2Ban does when a ban threshold is reached. The default is to add a firewall rule blocking the IP. Multiple actions can be chained together.

iptables-multiport (Legacy, Ubuntu 20.04 and older)

[DEFAULT]
banaction = iptables-multiport

Adds an iptables rule to the f2b-sshd chain dropping packets from the banned IP on the specified ports.

nftables-multiport (Modern, Ubuntu 22.04+)

[DEFAULT]
banaction = nftables-multiport

Creates an nftables set for each jail and adds banned IPs to the set. More efficient than iptables for large ban lists — nftables uses hash sets rather than linear rule chains.

cloudflare Action (Ban at CDN Level)

If your server is behind Cloudflare, you can ban IPs at the Cloudflare level rather than (or in addition to) the server firewall. This is particularly effective because it stops traffic before it reaches your server at all.

Configure Cloudflare credentials in /etc/fail2ban/action.d/cloudflare.conf (already included with Fail2Ban, just configure it):

# In jail.local for a specific jail:
[wordpress-login]
action = nftables-multiport
         cloudflare

sendmail-whois: Email Notification with WHOIS Lookup

[DEFAULT]
destemail = [email protected]
sender = [email protected]
mta = sendmail
action = %(action_mwl)s

The mwl action bans the IP, sends an email notification, and includes a WHOIS lookup of the attacker's IP. Useful for high-severity jails (SSH, admin login).

Custom Action Example: Telegram Notification

Create /etc/fail2ban/action.d/telegram.conf:

[Definition]
actionban = curl -s -X POST \
  "https://api.telegram.org/bot<TOKEN>/sendMessage" \
  -d chat_id="<CHAT_ID>" \
  -d text="[Fail2Ban] <name>: <ip> banned for <bantime>s"

actionunban = curl -s -X POST \
  "https://api.telegram.org/bot<TOKEN>/sendMessage" \
  -d chat_id="<CHAT_ID>" \
  -d text="[Fail2Ban] <name>: <ip> unbanned"

8. Advanced Configuration

Progressive Banning with the Recidive Jail

The recidive jail is a meta-jail: it monitors Fail2Ban's own log and applies progressively longer bans to IPs that keep getting banned across any jail. An IP that has been banned 3 times in 24 hours gets a 7-day ban.

[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800
findtime = 86400
maxretry = 3

This is one of the most effective configurations for dealing with persistent attackers who rotate through short bans.

Ban Time Increment

Fail2Ban supports exponentially increasing ban times for repeat offenders:

[DEFAULT]
bantime.increment = true
bantime.factor = 1
bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
bantime.multiplier = 1 2 4 8 16 32 64
bantime.maxtime = 1w

First ban: 1 hour. Second ban: 2 hours. Third ban: 4 hours. Each subsequent offense doubles the ban duration up to 1 week.

Persistent Bans Across Reboots

By default, Fail2Ban restores bans from the SQLite database on startup. Ensure dbfile and dbpurgeage are configured appropriately:

[DEFAULT]
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 604800

IPv6 Support

[DEFAULT]
# Enable IPv6 ban support
banaction = nftables-multiport
banaction_allports = nftables-allports

nftables natively handles both IPv4 and IPv6 in the same ruleset. If you are still on iptables, you need separate configurations for ip6tables.

Ignoreip: Critical Safety Configuration

Always add your management IP addresses, monitoring services, and internal networks to ignoreip:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
           203.0.113.10
           10.0.0.0/8
           172.16.0.0/12
           192.168.0.0/16

If your IP is not static, consider using a VPN or adding a secondary authentication method rather than leaving ignoreip empty.

Custom Email Alerts per Jail

[sshd]
enabled = true
action = nftables-multiport
         sendmail-whois[name=SSH, [email protected], [email protected]]

9. Management Commands: Your Daily Toolkit

The fail2ban-client command is the primary interface for managing Fail2Ban at runtime. All changes take effect immediately without a service restart.

Overview and Status

# Show all active jails and their status
fail2ban-client status

# Detailed status for a specific jail
fail2ban-client status sshd

# Output includes: filter, currently banned IPs, total bans, log files monitored

Ban and Unban Operations

# Manually ban an IP in a specific jail
fail2ban-client set sshd banip 203.0.113.42

# Manually unban an IP (useful if you accidentally ban yourself)
fail2ban-client set sshd unbanip 203.0.113.42

# Check if an IP is currently banned
fail2ban-client get sshd banned | grep 203.0.113.42

# Unban an IP across ALL jails at once
fail2ban-client unban 203.0.113.42

Configuration Management

# Reload all jails (picks up jail.local changes)
fail2ban-client reload

# Reload a specific jail only
fail2ban-client reload sshd

# Start/stop a specific jail without restarting fail2ban
fail2ban-client start wordpress-login
fail2ban-client stop wordpress-login

Testing Filters

# Test a filter against a log file
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

# Test a custom filter (inline regex, useful for quick checks)
fail2ban-regex /var/log/nginx/access.log "^<HOST> .+ \"POST /wp-login\.php"

# Show only matched lines (verbose output)
fail2ban-regex --print-all-matched /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-login.conf

Ban History and Log Analysis

# All bans in the current log
grep "Ban" /var/log/fail2ban.log

# Bans including rotated logs
zgrep "Ban" /var/log/fail2ban.log*

# Most frequently banned IPs (top 20)
zgrep "Ban" /var/log/fail2ban.log* | grep -oP "\d+\.\d+\.\d+\.\d+" | sort | uniq -c | sort -rn | head -20

# Ban count per jail
zgrep "Ban" /var/log/fail2ban.log* | grep -oP "\[.+?\]" | sort | uniq -c | sort -rn

10. Monitoring and Reporting

Real-Time Log Monitoring

# Watch bans and unbans in real time
tail -f /var/log/fail2ban.log | grep --line-buffered -E "(Ban|Unban|Found)"

Daily Attack Summary Script

Save this script as /etc/cron.daily/fail2ban-report and make it executable:

#!/bin/bash
# Daily Fail2Ban summary — runs via cron.daily

YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
LOG="/var/log/fail2ban.log"
ADMIN="[email protected]"

BANS=$(grep "Ban" $LOG | grep "$YESTERDAY" | wc -l)
TOP_IPS=$(grep "Ban" $LOG | grep "$YESTERDAY" | grep -oP "\d+\.\d+\.\d+\.\d+" | sort | uniq -c | sort -rn | head -10)
TOP_JAILS=$(grep "Ban" $LOG | grep "$YESTERDAY" | grep -oP "\[.+?\]" | sort | uniq -c | sort -rn)

BODY="Fail2Ban Daily Report: $YESTERDAY\n\nTotal bans: $BANS\n\nTop IPs:\n$TOP_IPS\n\nBans per jail:\n$TOP_JAILS"

echo -e "$BODY" | mail -s "Fail2Ban Daily Report" $ADMIN
chmod +x /etc/cron.daily/fail2ban-report

Prometheus Integration

Fail2Ban exposes metrics that can be scraped by Prometheus via the fail2ban-exporter. This allows you to track ban rates, active bans per jail, and banned IP counts in Grafana dashboards alongside your other server metrics.

pip3 install fail2ban-prometheus-exporter
fail2ban-exporter --port 9191 --socket /var/run/fail2ban/fail2ban.sock &

Add a scrape target to your Prometheus config:

scrape_configs:
  - job_name: 'fail2ban'
    static_configs:
      - targets: ['localhost:9191']

GeoIP Analysis of Attackers

# Install geoiplookup
apt install geoip-bin

# Analyze top banned IPs by country
zgrep "Ban" /var/log/fail2ban.log* | grep -oP "\d+\.\d+\.\d+\.\d+" | sort -u | while read ip; do
    geoiplookup "$ip" 2>/dev/null | grep -oP "(?<=: )[A-Z]{2},"
done | sort | uniq -c | sort -rn | head -20

11. Common Mistakes That Get Admins Locked Out

Fail2Ban configuration errors can range from annoying (jails not triggering) to catastrophic (locking yourself out of a remote server). Here are the most common mistakes and how to avoid them.

Mistake 1: Editing jail.conf Instead of jail.local

Every package upgrade overwrites jail.conf. All custom configuration belongs in jail.local. If you edit jail.conf, your changes will disappear silently on the next apt upgrade.

Mistake 2: Forgetting to Whitelist Your Own IP

This is how admins lock themselves out of remote servers. Before enabling any jail with low maxretry thresholds, add your IP to ignoreip in [DEFAULT]. If you do get locked out, you need out-of-band access (console, KVM, rescue mode) to run:

fail2ban-client unban YOUR_IP

Mistake 3: Wrong Log Path

If logpath points to a non-existent file, Fail2Ban silently ignores the jail. Check that your log file exists and is being written to:

ls -la /var/log/nginx/access.log
tail -5 /var/log/nginx/access.log

Some setups use per-domain log files. Fail2Ban supports glob patterns in logpath:

logpath = /var/log/nginx/*.access.log

Mistake 4: Regex Not Matching

Always test your filter with fail2ban-regex before deploying. A filter that never matches is silently inactive — no errors, no bans, no protection. Common reasons for regex failures:

  • Log format differences between Nginx/Apache versions
  • Custom log format in your Nginx config that differs from the default
  • Missing escape characters (periods in paths must be \.)
  • Forgetting that Fail2Ban uses Python regex, not grep regex

Mistake 5: Too Aggressive Settings

Setting maxretry = 1 on a login page means a single typo bans the user for hours. For user-facing services, balance security with usability. Use maxretry = 3-5 for login pages and reserve maxretry = 1 for known-malicious patterns (vulnerability scanners, xmlrpc abuse).

Mistake 6: Mismatched Firewall Backend

If you configure banaction = nftables-multiport but your system uses iptables (or vice versa), bans will fail silently. Check your system:

# Check which is active
nft list ruleset 2>/dev/null | head -5 || echo "nftables not active"
iptables -L 2>/dev/null | head -5 || echo "iptables not active"

Mistake 7: Forgetting to Reload After Config Changes

Editing jail.local does not automatically apply changes. Always run:

fail2ban-client reload

Or for a full restart:

systemctl restart fail2ban

Mistake 8: Not Monitoring the Fail2Ban Log

Fail2Ban itself logs errors (regex failures, file not found, action errors) to /var/log/fail2ban.log. Check this file regularly, especially after making configuration changes.


12. Complete Production Configuration

Here is a complete, battle-tested /etc/fail2ban/jail.local configuration suitable for a production server running SSH, Nginx, and WordPress:

[DEFAULT]
# ─────────────────────────────────────────────────────
# Global defaults — override per jail as needed
# ─────────────────────────────────────────────────────

# Ban duration (start with 1 hour, use recidive for repeat offenders)
bantime = 3600

# Time window to count failures
findtime = 600

# Failures before ban
maxretry = 5

# Firewall backend — use nftables on Ubuntu 22.04+
banaction = nftables-multiport

# Email notifications
destemail = [email protected]
sender = [email protected]
mta = sendmail

# Ban database — persist across restarts
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 604800

# Whitelist — ALWAYS include your admin IPs
ignoreip = 127.0.0.1/8 ::1
           # Add your IP: 203.0.113.10

# Progressive ban increments
bantime.increment = true
bantime.multiplier = 1 2 4 8 16 32 64
bantime.maxtime = 1w

# ─────────────────────────────────────────────────────
# SSH Protection
# ─────────────────────────────────────────────────────
[sshd]
enabled = true
port = ssh,2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
findtime = 300
mode = aggressive

# ─────────────────────────────────────────────────────
# Nginx Protection
# ─────────────────────────────────────────────────────
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5

[nginx-4xx]
enabled = true
filter = nginx-4xx
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 60
bantime = 3600

[nginx-badbots]
enabled = true
filter = nginx-badbots
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 1
bantime = 86400
findtime = 60

[nginx-login]
enabled = true
filter = nginx-login
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 7200

# ─────────────────────────────────────────────────────
# WordPress Protection
# ─────────────────────────────────────────────────────
[wordpress-login]
enabled = true
filter = wordpress-login
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600

[wordpress-admin]
enabled = true
filter = wordpress-admin
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 600
bantime = 3600

# ─────────────────────────────────────────────────────
# Repeat Offender Progressive Ban
# ─────────────────────────────────────────────────────
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800
findtime = 86400
maxretry = 3

Quick Reference

Jail Protects Log File Recommended maxretry Recommended bantime
sshd SSH login /var/log/auth.log 3 24h (86400s)
nginx-http-auth HTTP Basic Auth /var/log/nginx/error.log 5 1h (3600s)
nginx-4xx Vulnerability scanners /var/log/nginx/access.log 20 1h (3600s)
nginx-badbots Known malicious user-agents /var/log/nginx/access.log 1 24h (86400s)
nginx-login Admin/app login pages /var/log/nginx/access.log 5 2h (7200s)
wordpress-login wp-login.php + xmlrpc.php /var/log/nginx/access.log 5 1h (3600s)
wordpress-admin wp-admin directory /var/log/nginx/access.log 10 1h (3600s)
recidive Repeat offenders (any jail) /var/log/fail2ban.log 3 7 days (604800s)

Frequently Asked Questions

Does Fail2Ban replace a firewall?

No. Fail2Ban reacts to patterns in log files and adds temporary firewall rules for offending IPs. It needs an underlying firewall (nftables, iptables, or ufw) to actually enforce the ban — Fail2Ban itself has no packet-filtering engine of its own.

Will aggressive Fail2Ban settings lock out real users?

They can, which is why ignoreip should always include your own management IPs before you tighten maxretry or enable mode = aggressive on the SSH jail. Test custom filters with fail2ban-regex against real log data before enabling a jail in blocking mode.

Do bans survive a server reboot?

Only if the SQLite ban database is enabled (dbfile and dbpurgeage in the [DEFAULT] section, covered in section 12). Without it, every restart clears active bans and repeat offenders start their ban count from zero.

Is Fail2Ban enough security on its own?

No. It stops brute-force and scanning traffic at the firewall level, but it does not inspect request content the way a WAF does, and it cannot stop an attacker who already has valid credentials. Pair it with ModSecurity for application-layer attacks and SSH key authentication to make password brute-forcing irrelevant in the first place.

Conclusion: Fail2Ban Is Necessary but Not Sufficient

Fail2Ban is an essential layer in any server security strategy. Properly configured, it eliminates the majority of brute force attacks against SSH, web applications, and login endpoints automatically, around the clock, without manual intervention.

But it is one layer in a defense-in-depth architecture. Fail2Ban cannot protect against:

  • Attacks from distributed botnets using thousands of unique IPs (rate limiting + WAF handles this)
  • Authenticated attacker sessions (that is a different problem domain)
  • Application-layer vulnerabilities like SQL injection or XSS (ModSecurity WAF handles this)
  • Zero-day exploits in your web applications

The complete defense stack looks like this:

  • Fail2Ban — Block brute force and scanning at the firewall level
  • nftables/UFW — Whitelist only necessary ports, block everything else
  • SSH key authentication — Make password brute force irrelevant for SSH
  • ModSecurity WAF — Block injection attacks, XSS, and OWASP Top 10
  • ClamAV — Scan uploaded files for malware
  • Regular updates — Patch known vulnerabilities before they are exploited

Panelica integrates all of these layers into a single management interface. Fail2Ban, ModSecurity, ClamAV, nftables, and SSH security are all configured, monitored, and managed through the Panelica Security dashboard, so you are not maintaining five separate config files across five separate tools.

If you are managing multiple servers, managing security manually across each of them is exactly the kind of error-prone repetitive work that Panelica was built to eliminate. Every configuration shown in this guide is handled automatically by Panelica for every server and every user on your infrastructure.

Start with the basics — install Fail2Ban, configure the sshd jail, test it, expand from there. The configuration shown in this guide should be your starting point, not your final destination. Monitor your logs, watch what gets banned, and tune your filters as you learn the attack patterns specific to your services.

Security is an ongoing practice, not a one-time setup.

Security-first hosting panel

Run your servers on a modern panel.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Migration from cPanel, made simple.