Master fail2ban for production security hardening. Learn how to build intelligent, automated defense systems that detect and block attacks in real-time across SSH, web servers, databases, and custom applications.

Static firewall rules protect against known threats, but they can't adapt to active attacks. A brute force attempt on SSH, credential stuffing against your API, or application-level exploits require dynamic response—detecting malicious patterns in real-time and blocking the source before damage occurs.
fail2ban is an intrusion prevention framework that monitors log files, detects malicious behavior using regex patterns, and automatically updates firewall rules to ban offending IPs. It's the dynamic layer on top of your static iptables rules, turning your server into an adaptive defense system.
This isn't about installing fail2ban and forgetting it. We're going deep into the architecture, filter and action system, jail configuration, performance tuning, and building custom detection rules for your specific applications. By the end, you'll have production-grade intrusion prevention that protects SSH, web servers, databases, APIs, and custom services.
fail2ban operates on a simple but powerful concept: watch log files, match patterns, take action.
The architecture has four core components:
Filters - Regex patterns that match malicious behavior in log files. Each filter defines what "bad" looks like—failed login attempts, 404 scans, SQL injection patterns, etc.
Actions - What to do when a match is found. Usually this means adding an iptables rule to ban the IP, but actions can also send emails, update cloud firewall rules, or trigger custom scripts.
Jails - The combination of a filter, action, and configuration. A jail monitors specific log files using a filter and executes actions when thresholds are exceeded. You might have separate jails for SSH, nginx, Apache, MySQL, and custom applications.
Backends - How fail2ban reads log files. Options include polling (check file periodically), pyinotify (kernel-level file change notifications), gamin (file alteration monitor), and systemd journal integration.
The flow: fail2ban's backend monitors log files → filter regex matches malicious patterns → jail counts failures per IP → threshold exceeded → action executes (ban IP) → IP is blocked for configured time → automatic unban after ban time expires.
Think of fail2ban as a security analyst that never sleeps, constantly reading logs, identifying threats, and responding instantly.
Let's start with installation and understand the configuration structure.
apt-get update
apt-get install fail2ban
systemctl enable fail2ban
systemctl start fail2banConfiguration files live in /etc/fail2ban/:
fail2ban.conf - Global fail2ban settings (don't edit directly)fail2ban.local - Local overrides for fail2ban.confjail.conf - Default jail configurations (don't edit directly)jail.local - Local jail configurations (this is where you work)filter.d/ - Filter definitions (regex patterns)action.d/ - Action definitions (what to do on match)The .local files override .conf files. Never edit .conf files directly—they're overwritten on updates. Always create .local files with your customizations.
A jail is the fundamental unit of fail2ban configuration. Let's dissect a complete jail configuration:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
action = iptables-multiport[name=SSH, port="ssh", protocol=tcp]Breaking this down:
enabled = true - Activate this jailport = ssh - Which port to ban (ssh resolves to 22)filter = sshd - Use the sshd filter from /etc/fail2ban/filter.d/sshd.conflogpath = /var/log/auth.log - Which log file to monitormaxretry = 3 - How many failures before banfindtime = 600 - Time window in seconds (10 minutes)bantime = 3600 - How long to ban in seconds (1 hour)action = ... - What to do on banThe logic: if an IP has 3 failures within 10 minutes, ban it for 1 hour.
findtime is critical. It's a sliding window. If an IP fails at minute 0, 5, and 11, that's only 2 failures in the 10-minute window when the third attempt happens at minute 11 (the first failure at minute 0 is outside the window). Understanding this prevents confusion about why IPs aren't being banned.
The most common attack vector is SSH brute force. Let's build production-grade SSH protection:
[DEFAULT]
# Global settings for all jails
bantime = 1h
findtime = 10m
maxretry = 3
destemail = security@example.com
sender = fail2ban@example.com
action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 24h
findtime = 1hThe [DEFAULT] section sets global values. Individual jails inherit these but can override them.
The action = %(action_mwl)s is a predefined action that:
Other predefined actions:
%(action_)s - Just ban, no email%(action_mw)s - Ban and email with whois%(action_mwl)s - Ban, email with whois and log linesFor production, you might want stricter SSH protection:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 2
bantime = -1
findtime = 30mbantime = -1 means permanent ban. The IP is never automatically unbanned. Use this carefully—you might lock out legitimate users who fat-finger passwords.
Web servers face different attacks: directory traversal, SQL injection attempts, 404 scanning, bot traffic, and application-specific exploits.
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 1h
[nginx-noscript]
enabled = true
port = http,https
filter = nginx-noscript
logpath = /var/log/nginx/access.log
maxretry = 6
bantime = 1h
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 24h
[nginx-noproxy]
enabled = true
port = http,https
filter = nginx-noproxy
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 24h
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 1m
bantime = 1hThese jails protect against:
nginx-http-auth - Failed HTTP basic auth attemptsnginx-noscript - Attempts to execute scripts (.php, .asp, etc.) on static sitesnginx-badbots - Known malicious user agentsnginx-noproxy - Proxy abuse attemptsnginx-limit-req - Rate limit violations (requires nginx limit_req_zone)[apache-auth]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 3
bantime = 1h
[apache-badbots]
enabled = true
port = http,https
filter = apache-badbots
logpath = /var/log/apache2/access.log
maxretry = 2
bantime = 24h
[apache-noscript]
enabled = true
port = http,https
filter = apache-noscript
logpath = /var/log/apache2/error.log
maxretry = 6
bantime = 1h
[apache-overflows]
enabled = true
port = http,https
filter = apache-overflows
logpath = /var/log/apache2/error.log
maxretry = 2
bantime = 24h
[apache-modsecurity]
enabled = true
filter = apache-modsecurity
port = http,https
logpath = /var/log/apache2/modsec_audit.log
maxretry = 2
bantime = 24hDatabases are high-value targets. Protect them with fail2ban:
[mysqld-auth]
enabled = true
filter = mysqld-auth
port = 3306
logpath = /var/log/mysql/error.log
maxretry = 3
bantime = 1h
findtime = 10mYou'll need a custom filter for MySQL. Create /etc/fail2ban/filter.d/mysqld-auth.conf:
[Definition]
failregex = ^%(__prefix_line)s.*Access denied for user.*\(using password: YES\).*<HOST>
^%(__prefix_line)s.*Access denied for user.*\(using password: NO\).*<HOST>
ignoreregex =[postgresql]
enabled = true
filter = postgresql
port = 5432
logpath = /var/log/postgresql/postgresql-*-main.log
maxretry = 3
bantime = 1hPostgreSQL filter /etc/fail2ban/filter.d/postgresql.conf:
[Definition]
failregex = ^.*FATAL: password authentication failed for user.*<HOST>
^.*FATAL: no pg_hba.conf entry for host.*<HOST>
ignoreregex =The real power of fail2ban is protecting custom applications. Let's build filters for common scenarios.
Protect your API from abuse. Assume your application logs failed auth attempts:
2026-03-05 10:23:45 [ERROR] Authentication failed for IP 203.0.113.45 - Invalid API keyCreate filter /etc/fail2ban/filter.d/api-auth.conf:
[Definition]
failregex = ^\S+ \S+ \[ERROR\] Authentication failed for IP <HOST> - Invalid API key
^\S+ \S+ \[ERROR\] Rate limit exceeded for IP <HOST>
^\S+ \S+ \[ERROR\] Suspicious activity detected from <HOST>
ignoreregex =Configure the jail:
[api-auth]
enabled = true
filter = api-auth
port = http,https
logpath = /var/log/myapp/api.log
maxretry = 5
findtime = 5m
bantime = 30m
action = iptables-multiport[name=API, port="http,https", protocol=tcp]WordPress sites face constant attack. Protect wp-login.php and xmlrpc.php:
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php
^<HOST> .* "POST /xmlrpc.php
ignoreregex =Jail configuration:
[wordpress]
enabled = true
filter = wordpress
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 3
findtime = 10m
bantime = 4hThis bans IPs that POST to wp-login.php or xmlrpc.php more than 3 times in 10 minutes.
Detect SQL injection attempts in application logs:
[Definition]
failregex = ^\S+ \S+ \[SECURITY\] SQL injection attempt from <HOST>
^\S+ \S+ \[SECURITY\] Malicious payload detected from <HOST>
^\S+ \S+ \[ERROR\] Database error.*<HOST>.*UNION SELECT
^\S+ \S+ \[ERROR\] Database error.*<HOST>.*OR 1=1
ignoreregex =Filters are the brain of fail2ban. Understanding regex patterns and filter options is crucial for effective detection.
A complete filter with all options:
[INCLUDES]
before = common.conf
[Definition]
_daemon = sshd
failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|error|failed) for .* from <HOST>( via \S+)?\s*$
^%(__prefix_line)sFailed (?:password|publickey) for .* from <HOST>(?: port \d*)?(?: ssh\d*)?$
^%(__prefix_line)sROOT LOGIN REFUSED.* FROM <HOST>\s*$
^%(__prefix_line)s[iI](?:llegal|nvalid) user .* from <HOST>\s*$
ignoreregex =
[Init]
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
datepattern = {^LN-BEG}Key components:
[INCLUDES] - Import common definitions. common.conf provides __prefix_line which matches common log prefixes (timestamps, hostnames, etc.).
failregex - Patterns that match failures. Multiple patterns can be defined. <HOST> is a special placeholder that matches IPv4/IPv6 addresses.
ignoreregex - Patterns to explicitly ignore. Useful for excluding monitoring systems or known safe IPs from pattern matching.
[Init] - Backend-specific initialization. journalmatch is for systemd journal filtering.
datepattern - How to parse timestamps. Usually inherited from common.conf.
Before deploying a filter, test it against actual log data:
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.confThis shows:
Example output:
127 matches out of 1247 lines. The filter is working.
For application logs with custom formats:
[Definition]
# Match JSON logs
failregex = ^\{"timestamp":"[^"]+","level":"ERROR","ip":"<HOST>","message":"Authentication failed"
^\{"timestamp":"[^"]+","level":"WARN","ip":"<HOST>","message":"Suspicious activity"
# Match structured logs with multiple fields
failregex = ^time=\S+ level=error ip=<HOST> msg="login failed" user=\S+ attempts=\d+
# Match logs with variable spacing
failregex = ^\s*\[\S+\]\s+ERROR\s+<HOST>\s+-\s+Failed\s+login\s+attempt
# Match multiline patterns (use with caution - performance impact)
failregex = ^<HOST> - - \[.*\] "POST /login
^\s+Response: 401 UnauthorizedThe __prefix_line variable from common.conf matches standard syslog prefixes:
Mar 5 10:23:45 hostname sshd[12345]: Failed password for user from 203.0.113.45It matches: Mar 5 10:23:45 hostname sshd[12345]:
For custom prefixes:
[Definition]
__prefix_line = ^%(__prefix_time)s\s+%(__prefix_host)s\s+myapp\[\d+\]:\s+
failregex = ^%(__prefix_line)sAuthentication failed for <HOST>Actions define what happens when a ban occurs. While iptables is most common, fail2ban supports many action types.
Actions are defined in /etc/fail2ban/action.d/. Here's a simplified iptables action:
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
<iptables> -F f2b-<name>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
[Init]
name = default
protocol = tcp
chain = INPUT
blocktype = REJECT --reject-with icmp-port-unreachable
returntype = RETURN
iptables = iptablesactionstart - Executed when fail2ban starts. Creates the iptables chain.
actionstop - Executed when fail2ban stops. Removes the chain.
actioncheck - Verifies the action is properly configured.
actionban - Executed when an IP is banned. Adds iptables rule.
actionunban - Executed when ban expires. Removes iptables rule.
Create custom actions for your infrastructure. Example: update AWS Security Group:
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = aws ec2 authorize-security-group-ingress \
--group-id <security_group_id> \
--protocol tcp \
--port <port> \
--cidr <ip>/32 \
--description "fail2ban ban"
actionunban = aws ec2 revoke-security-group-ingress \
--group-id <security_group_id> \
--protocol tcp \
--port <port> \
--cidr <ip>/32
[Init]
security_group_id = sg-0123456789abcdef
port = 22Use in jail:
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
action = aws-security-group[security_group_id="sg-0123456789abcdef", port="22"]Send alerts to Slack, Discord, or PagerDuty:
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -X POST <slack_webhook_url> \
-H 'Content-Type: application/json' \
-d '{"text":"fail2ban: Banned <ip> for <failures> failures on <name>"}'
actionunban = curl -X POST <slack_webhook_url> \
-H 'Content-Type: application/json' \
-d '{"text":"fail2ban: Unbanned <ip> on <name>"}'
[Init]
slack_webhook_url = https://hooks.slack.com/services/YOUR/WEBHOOK/URLExecute multiple actions on ban:
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
action = iptables-multiport[name=SSH, port="ssh", protocol=tcp]
slack-notify[name=SSH]
sendmail-whois[name=SSH, dest=security@example.com]This bans with iptables, sends Slack notification, and emails security team.
Never ban your own IPs or monitoring systems.
In /etc/fail2ban/jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
10.0.0.0/8
192.168.1.0/24
203.0.113.50This prevents banning:
Override global whitelist for specific jails:
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
ignoreip = 127.0.0.1/8 10.0.0.0/8 203.0.113.100For IPs that should never be banned, use ignorecommand:
[DEFAULT]
ignorecommand = /usr/local/bin/fail2ban-whitelist-check.sh <ip>Script /usr/local/bin/fail2ban-whitelist-check.sh:
#!/bin/bash
IP=$1
# Check if IP is in whitelist database
if grep -q "^$IP$" /etc/fail2ban/whitelist.txt; then
exit 0 # Ignore this IP
fi
# Check if IP belongs to trusted ASN
ASN=$(whois -h whois.cymru.com " -v $IP" | tail -n1 | awk '{print $1}')
if [[ "$ASN" == "15169" ]]; then # Google ASN
exit 0
fi
exit 1 # Don't ignoreMake it executable:
chmod +x /usr/local/bin/fail2ban-whitelist-check.shfail2ban can impact performance on high-traffic servers. Optimize it.
Choose the right backend for log monitoring:
[DEFAULT]
# Options: auto, pyinotify, gamin, polling, systemd
backend = systemdsystemd - Best for systemd-based systems. Uses journal directly, no file polling.
pyinotify - Uses kernel inotify for file changes. Efficient, requires python-pyinotify package.
polling - Checks files periodically. Least efficient, most compatible.
auto - Automatically selects best available backend.
For systemd systems, use systemd backend:
[sshd]
enabled = true
filter = sshd
backend = systemd
maxretry = 3The filter needs journalmatch:
[Definition]
failregex = ^.*Failed password for .* from <HOST>
[Init]
journalmatch = _SYSTEMD_UNIT=sshd.serviceBy default, fail2ban uses pickle files for persistence. For better performance, use SQLite:
[Definition]
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 86400dbpurgeage removes old ban records after 24 hours.
Reduce logging verbosity in production:
[Definition]
loglevel = INFO
logtarget = /var/log/fail2ban.logOptions: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG
Use INFO or WARNING in production. DEBUG generates massive logs.
Balance security and performance:
[DEFAULT]
# Short findtime = less memory usage
findtime = 10m
# Longer bantime = fewer unban operations
bantime = 1h
# Increase for high-traffic services
maxretry = 5Shorter findtime means fail2ban tracks fewer historical failures, reducing memory usage.
Production systems need visibility into fail2ban operations.
# Overall status
fail2ban-client status
# Specific jail status
fail2ban-client status sshd
# Show banned IPs
fail2ban-client status sshd | grep "Banned IP"Example output:
# Ban an IP manually
fail2ban-client set sshd banip 203.0.113.45
# Unban an IP
fail2ban-client set sshd unbanip 203.0.113.45
# Unban all IPs in a jail
fail2ban-client unban --allAfter changing configuration:
# Reload all jails
fail2ban-client reload
# Reload specific jail
fail2ban-client reload sshd
# Restart fail2ban service
systemctl restart fail2banTip
Use reload instead of restart to keep existing bans active. Restart clears all bans.
Monitor fail2ban logs:
# Real-time log monitoring
tail -f /var/log/fail2ban.log
# Show recent bans
grep "Ban" /var/log/fail2ban.log | tail -20
# Show recent unbans
grep "Unban" /var/log/fail2ban.log | tail -20
# Count bans per jail
grep "Ban" /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rnExport fail2ban metrics for monitoring systems:
#!/bin/bash
# /usr/local/bin/fail2ban-metrics.sh
for jail in $(fail2ban-client status | grep "Jail list" | sed "s/.*://;s/,//g"); do
banned=$(fail2ban-client status $jail | grep "Currently banned" | awk '{print $NF}')
total=$(fail2ban-client status $jail | grep "Total banned" | awk '{print $NF}')
echo "fail2ban_currently_banned{jail=\"$jail\"} $banned"
echo "fail2ban_total_banned{jail=\"$jail\"} $total"
doneIntegrate with Prometheus node_exporter textfile collector:
/usr/local/bin/fail2ban-metrics.sh > /var/lib/node_exporter/textfile_collector/fail2ban.promFor cloud infrastructure, integrate fail2ban with native firewall services.
[Definition]
actionban = aws wafv2 update-ip-set \
--name fail2ban-blocklist \
--scope REGIONAL \
--id <ip_set_id> \
--addresses <ip>/32 \
--region <region>
actionunban = aws wafv2 update-ip-set \
--name fail2ban-blocklist \
--scope REGIONAL \
--id <ip_set_id> \
--addresses "" \
--region <region>
[Init]
ip_set_id = your-ip-set-id
region = us-east-1[Definition]
actionban = curl -X POST "https://api.cloudflare.com/client/v4/zones/<zone_id>/firewall/access_rules/rules" \
-H "Authorization: Bearer <api_token>" \
-H "Content-Type: application/json" \
--data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"fail2ban"}'
actionunban = curl -X DELETE "https://api.cloudflare.com/client/v4/zones/<zone_id>/firewall/access_rules/rules/<rule_id>" \
-H "Authorization: Bearer <api_token>"
[Init]
zone_id = your-zone-id
api_token = your-api-token[Definition]
actionban = gcloud compute firewall-rules create fail2ban-<ip> \
--direction=INGRESS \
--priority=1000 \
--network=default \
--action=DENY \
--rules=all \
--source-ranges=<ip>/32 \
--description="fail2ban ban"
actionunban = gcloud compute firewall-rules delete fail2ban-<ip> --quiet
[Init]Deploying untested filters leads to false positives or no matches at all. Always test:
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.confForgetting to whitelist your management IPs. Always add your IPs to ignoreip:
[DEFAULT]
ignoreip = 127.0.0.1/8 YOUR.MANAGEMENT.IP.ADDRESSmaxretry = 1 with findtime = 1m will ban legitimate users who mistype passwords. Balance security with usability:
maxretry = 3
findtime = 10m
bantime = 1hfail2ban can fail silently. Monitor its status:
#!/bin/bash
if ! systemctl is-active --quiet fail2ban; then
echo "fail2ban is not running!"
systemctl start fail2ban
# Send alert
fifail2ban can lose track of log files during rotation. Ensure proper logrotate configuration:
/var/log/fail2ban.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 root adm
postrotate
fail2ban-client flushlogs >/dev/null 2>&1 || true
endscript
}Here's a complete, production-ready fail2ban configuration:
Supporting filter for API protection /etc/fail2ban/filter.d/api-auth.conf:
[Definition]
failregex = ^\S+ \S+ \[ERROR\] Authentication failed for IP <HOST>
^\S+ \S+ \[ERROR\] Rate limit exceeded for IP <HOST>
^\S+ \S+ \[SECURITY\] Suspicious activity from <HOST>
^\S+ \S+ \[ERROR\] Invalid API key from <HOST>
ignoreregex =Check these in order:
fail2ban-client status | grep "Jail list"fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.confgrep ignoreip /etc/fail2ban/jail.localfail2ban-client status sshdtail -100 /var/log/fail2ban.log | grep ERRORLegitimate users getting banned:
maxretry and findtimeignoreipfail2ban consuming too many resources:
findtime to track fewer historical failuresBans disappear after fail2ban restart:
dbfile is writabledbpurgeage isn't too shortFor multi-server environments, synchronize bans across servers:
#!/bin/bash
# /usr/local/bin/fail2ban-sync.sh
SERVERS="server1.example.com server2.example.com server3.example.com"
JAIL="sshd"
# Get currently banned IPs
BANNED_IPS=$(fail2ban-client status $JAIL | grep "Banned IP list" | sed 's/.*://;s/\s//g')
# Sync to other servers
for SERVER in $SERVERS; do
if [ "$SERVER" != "$(hostname -f)" ]; then
for IP in $BANNED_IPS; do
ssh $SERVER "fail2ban-client set $JAIL banip $IP" 2>/dev/null
done
fi
doneRun via cron every minute:
* * * * * /usr/local/bin/fail2ban-sync.shBetter approach: Use a centralized ban database with Redis:
#!/bin/bash
REDIS_HOST="redis.example.com"
JAIL="sshd"
# Push local bans to Redis
BANNED=$(fail2ban-client status $JAIL | grep "Banned IP list" | sed 's/.*://')
for IP in $BANNED; do
redis-cli -h $REDIS_HOST SADD fail2ban:$JAIL:banned $IP
redis-cli -h $REDIS_HOST EXPIRE fail2ban:$JAIL:banned 3600
done
# Pull bans from Redis and apply locally
REDIS_BANNED=$(redis-cli -h $REDIS_HOST SMEMBERS fail2ban:$JAIL:banned)
for IP in $REDIS_BANNED; do
fail2ban-client set $JAIL banip $IP 2>/dev/null
doneBlock entire countries using GeoIP:
apt-get install geoip-bin geoip-databaseCreate filter /etc/fail2ban/filter.d/geoip-block.conf:
[Definition]
failregex = ^<HOST>$
ignoreregex =Create action /etc/fail2ban/action.d/geoip-block.conf:
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = COUNTRY=$(geoiplookup <ip> | awk -F': ' '{print $2}' | cut -d',' -f1)
if [ "$COUNTRY" = "CN" ] || [ "$COUNTRY" = "RU" ]; then
iptables -I INPUT -s <ip> -j DROP
fi
actionunban = iptables -D INPUT -s <ip> -j DROP
[Init]Protect specific API endpoints with custom filters:
[Definition]
# Match excessive requests to /api/expensive-operation
failregex = ^<HOST> .* "(?:GET|POST) /api/expensive-operation
ignoreregex =Jail configuration:
[api-endpoint-limit]
enabled = true
filter = api-endpoint-limit
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 1m
bantime = 10mCreate honeypot services that automatically ban anyone who connects:
[honeypot-ssh]
enabled = true
filter = honeypot-ssh
port = 2222
logpath = /var/log/honeypot.log
maxretry = 1
findtime = 1m
bantime = -1Honeypot filter:
[Definition]
failregex = ^.*Connection from <HOST>
ignoreregex =Run a fake SSH service on port 2222 that logs all connection attempts.
Increase ban time for repeat offenders:
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 10m
# First ban: 1 hour
bantime = 1h
# Increase ban time for repeat offenders
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 30dThis doubles the ban time for each subsequent ban, up to 30 days maximum.
fail2ban is one layer. Combine with:
Don't expose services unnecessarily:
# Only allow SSH from management network
iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
# Let fail2ban handle the restKeep fail2ban and filters updated:
apt-get update
apt-get upgrade fail2banNew filters are added regularly for emerging threats.
Regularly review fail2ban logs for patterns:
# Most banned IPs
grep "Ban" /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
# Most active jails
grep "Ban" /var/log/fail2ban.log | grep -oP '\[.*?\]' | sort | uniq -c | sort -rn
# Ban timeline
grep "Ban" /var/log/fail2ban.log | awk '{print $1, $2}' | uniq -cVersion control your fail2ban configuration:
cd /etc/fail2ban
git init
git add jail.local jail.d/ filter.d/ action.d/
git commit -m "Initial fail2ban configuration"fail2ban isn't always the right solution.
For API rate limiting, use application-level solutions (nginx limit_req, API gateways) instead of fail2ban. They're more granular and don't require log parsing.
fail2ban can't handle large-scale DDoS attacks. Use:
fail2ban reacts to attacks after they happen. For proactive blocking, integrate threat intelligence feeds directly into your firewall.
If you're seeing thousands of attacks per second, fail2ban's log parsing will be too slow. Use kernel-level solutions like eBPF-based filtering or hardware firewalls.
fail2ban transforms static firewall rules into an adaptive defense system. By monitoring logs, detecting patterns, and automatically responding to threats, it provides the dynamic security layer that production systems need.
The key principles:
Start with the production configuration example, customize filters for your applications, and tune thresholds based on your traffic patterns. Combined with strong authentication, network segmentation, and regular updates, fail2ban significantly hardens your infrastructure against the constant stream of attacks every internet-facing server faces.
Remember: fail2ban is reactive, not proactive. It's one layer in defense-in-depth. Use it alongside strong authentication, minimal attack surface, intrusion detection, and regular security audits to build truly resilient systems.