A collection of custom fail2ban filters for Apache and Nginx servers (Virtualmin, plain Apache, plain Nginx all supported). Designed for real-world attacks seen on production CakePHP, WordPress, and general web hosting environments.
Originally started as a bootstrap install script for
apache-wp-loginon Ubuntu 14.04 + Virtualmin (gist 18fcc234a324c858d50d0a96d5748ab4), and grew into this repo as more attack patterns were observed.
Despite the
apache-prefix, all filters work unchanged with Nginx. Both servers use identical combined log format by default, and the filters match on URL patterns, not server-specific syntax. See Works with Apache and Nginx below.
Understanding where bans are enforced helps you choose the right approach for your threat level:
| Level | Mechanism | Performance | What catches the traffic |
|---|---|---|---|
| Application (slowest) | Web server rewrite rules, .htaccess, PHP code |
Per-request overhead, still consumes PHP/worker | Apache/Nginx reads request, routes to handler, handler rejects |
| fail2ban + iptables (good) | Kernel netfilter INPUT chain, one rule per IP |
Linear scan per packet — slow with thousands of IPs | Packet hits firewall before it reaches any userspace process |
| fail2ban + ipset (best) | Kernel ipset hash lookup, O(1) | Can handle millions of IPs with no slowdown | Packet dropped at kernel level via hash table |
| Live IP feeds + ipset (proactive) | Spamhaus DROP, FireHOL, AbuseIPDB via cron | O(1) lookup, updated every few hours | Known-bad IPs blocked before they even probe |
- fail2ban filters (this repo) detect attack patterns in logs and ban offenders
- ipset as the ban backend — via
iptables-ipsetaction, so ban lookups are O(1) - Live blocklists (Spamhaus/FireHOL) for proactive blocking of known-bad netblocks
- iptables rules apply the ipset match on the
INPUTchain — kernel-level, before any web server sees the packet
See docs/ipset-blocklists.md for complete setup.
| Filter | What it catches | Default ban |
|---|---|---|
| apache-wp-login | Brute-force attempts on /wp-login.php |
1 hit = 10h ban |
| apache-script-hackbots | Bots probing for known vulnerable scripts (phpMyAdmin, shells, config files) — extensive list | 1 hit = 10h ban |
| apache-script-hackbot-shorter | Same as above but a shorter, lighter rule set | 1 hit = 10h ban |
| apache-url-enumeration | Bots generating thousands of deep nested URL path combinations (6+ segments returning 403/404) to enumerate routes on CakePHP/Laravel apps | 5 hits/min = 24h ban |
| apache-probe-paths | Comprehensive list of 1100+ attack/probe URLs captured from real attacks — VoIP SIP device configs (spa122.xml, Yealink, Polycom), PHP web shells, phpMyAdmin variants, CMS probes, shellshock/IFS injection, pChart2 traversal, IoT/router probes, scanner user-agents | 1 hit = 10h ban |
| nginx-proxy-abuse | Catches attempts to use your server as an open HTTP proxy (absolute URLs or CONNECT method in request line) | 1 hit = 7d ban |
sudo cp filter.d/*.conf /etc/fail2ban/filter.d/
sudo cat jail.conf >> /etc/fail2ban/jail.local
sudo fail2ban-client reloadDownload a single filter and add the matching jail:
# Example: install just the URL enumeration filter
sudo wget -O /etc/fail2ban/filter.d/apache-url-enumeration.conf \
https://raw.githubusercontent.com/acosonic/fail2ban_custom_rules/master/filter.d/apache-url-enumeration.confThen add to /etc/fail2ban/jail.local:
[apache-url-enumeration]
enabled = true
port = http,https
filter = apache-url-enumeration
logpath = /var/log/virtualmin/*access_log
maxretry = 5
findtime = 60
bantime = 86400
[apache-probe-paths]
enabled = true
port = http,https
filter = apache-probe-paths
logpath = /var/log/virtualmin/*access_log
maxretry = 1
findtime = 36000
bantime = 36000
[nginx-proxy-abuse]
enabled = true
port = http,https
filter = nginx-proxy-abuse
logpath = /var/log/nginx/*access.log
/var/log/virtualmin/*access_log
maxretry = 1
findtime = 3600
bantime = 604800Reload:
sudo fail2ban-client reloadDespite the apache- prefix, these filters work with both Apache and Nginx out of the box. Both servers use identical combined log format by default:
1.2.3.4 - - [18/Feb/2025:08:05:54 -0800] "GET /path HTTP/1.1" 403 428 "-" "UA"
The filters match on ^<HOST>.*URL-pattern, which is log-format-agnostic. The only thing you need to change is the logpath in jail.conf.
The default jail.conf uses Virtualmin log paths. Adjust for your setup:
| Server | Log path |
|---|---|
| Virtualmin | /var/log/virtualmin/*access_log |
| Apache default | /var/log/apache2/*access.log |
| Nginx | /var/log/nginx/*access.log |
| Multiple | /var/log/apache2/*access.log/var/log/nginx/*access.log (one per line) |
| Custom | Check your vhost config |
If you use a non-standard log format (e.g. JSON logs, custom log_format), the regex may need adjustment. The easiest approach:
- Test with
fail2ban-regex /path/to/your.log filter.d/apache-probe-paths.conf - If nothing matches, paste a sample log line into Claude / ChatGPT along with the filter and ask it to adapt the regex to your format. The filters are plain regex, easy to tweak.
root@host:~# fail2ban-client status apache-script-hackbots
Status for the jail: apache-script-hackbots
|- Filter
| |- Currently failed: 0
| |- Total failed: 264
| `- File list: /var/log/virtualmin/example.com_access_log
`- Actions
|- Currently banned: 9
|- Total banned: 10
`- Banned IP list: 209.95.51.11 109.70.100.20 134.175.26.138 ...
Other useful commands:
# Status of all jails
sudo fail2ban-client status
# Unban an IP
sudo fail2ban-client set apache-script-hackbots unbanip 1.2.3.4
# Test a filter against a log file (dry run)
sudo fail2ban-regex /var/log/virtualmin/example.com_access_log /etc/fail2ban/filter.d/apache-url-enumeration.confA complete jail.local combining these filters with standard protections:
[sshd]
enabled = true
maxretry = 3
bantime = 86400
[postfix-sasl]
enabled = true
port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s
action = iptables-multiport[name=postfix-sasl, port="smtp,ssmtp,465,submission,imap,imaps,pop3,pop3s"]
filter = postfix-sasl
logpath = /var/log/mail.log
maxretry = 2
findtime = 10800
bantime = -1
[postfix]
enabled = true
[dovecot]
enabled = true
[apache-wp-login]
enabled = true
port = http,https
filter = apache-wp-login
logpath = /var/log/virtualmin/*access_log
maxretry = 1
findtime = 36000
bantime = 36000
[apache-script-hackbots]
enabled = true
port = http,https
filter = apache-script-hackbots
logpath = /var/log/virtualmin/*access_log
maxretry = 1
findtime = 36000
bantime = 36000
[apache-url-enumeration]
enabled = true
port = http,https
filter = apache-url-enumeration
logpath = /var/log/virtualmin/*access_log
maxretry = 5
findtime = 60
bantime = 86400
[apache-probe-paths]
enabled = true
port = http,https
filter = apache-probe-paths
logpath = /var/log/virtualmin/*access_log
maxretry = 1
findtime = 36000
bantime = 36000
[nginx-proxy-abuse]
enabled = true
port = http,https
filter = nginx-proxy-abuse
logpath = /var/log/nginx/*access.log
/var/log/virtualmin/*access_log
maxretry = 1
findtime = 3600
bantime = 604800Fail2ban monitors log files and bans IPs that match filter patterns. When criteria match (e.g. 5 failed attempts within 60 seconds), it triggers an action — typically an iptables rule blocking that IP for a set duration.
Every IP address has an owner registered with a Regional Internet Registry (RIPE, ARIN, etc.) with a mandatory abuse contact email. If you're seeing persistent attacks, you can report them — the registry can take action against non-responsive owners.
These filters use regex matching against potentially large log files. On low-resource servers, consider:
- Using the
-shortervariant instead of the full hackbots list - Increasing
findtimeto reduce scan frequency - Running
fail2ban-regexto test performance before enabling
Extract banned IPs from fail2ban (live jails, log files, or saved status dumps) into an ipset for permanent blocking.
# From a live jail
sudo tools/fail2ban-to-ipset.sh --jail postfix-sasl --set fail2ban-sasl
# From a saved status dump (e.g. historical sample)
sudo tools/fail2ban-to-ipset.sh --file samples/postfix-sasl-attackers-2025.txt --set smtp-attackers
# From fail2ban.log
sudo tools/fail2ban-to-ipset.sh --log /var/log/fail2ban.log --set historical
# Dry run
sudo tools/fail2ban-to-ipset.sh --jail sshd --set test --dry-runThen enable blocking:
sudo iptables -I INPUT 1 -m set --match-set smtp-attackers src -j DROPfail2ban is reactive — it bans IPs after they trigger a rule. For proactive blocking of known-bad IPs (hijacked netblocks, botnets, compromised hosts), use ipset with live-updated feeds like Spamhaus DROP or FireHOL.
See docs/ipset-blocklists.md for a complete setup guide with:
- Recommended feeds (Spamhaus, FireHOL, AbuseIPDB, Emerging Threats)
- Update script with atomic swap (no blocking gaps)
- Integration with fail2ban so bans persist across restarts
Bidirectional integration with AbuseIPDB — report banned IPs to the community and pull their blocklist for proactive blocking.
Add the abuseipdb-report action to any jail to auto-report when fail2ban bans an IP:
sudo cp action.d/abuseipdb-report.conf /etc/fail2ban/action.d/
sudo cp action.d/fail2ban-abuseipdb-report.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/fail2ban-abuseipdb-report.sh
sudo mkdir -p /var/cache/fail2ban-abuseipdbThen in jail.local:
[apache-probe-paths]
# ... existing config ...
action = iptables-multiport[name=probe, port="http,https"]
abuseipdb-report[category=21, api_key=YOUR_API_KEY]Common category IDs: 15=Hacking, 18=Brute-Force, 19=Bad Web Bot, 21=Web App Attack, 22=SSH. Combine with comma: category="18,21".
Features:
- 15-min cooldown per IP (AbuseIPDB policy)
- Skips private/reserved IPs
- Includes up to 5 log lines as evidence (truncated to 900 chars)
- Logs to
/var/log/fail2ban-abuseipdb.log
Download AbuseIPDB's high-confidence bad IPs into an ipset for proactive blocking:
sudo cp tools/update-abuseipdb-blocklist.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/update-abuseipdb-blocklist.sh
# Run it
sudo ABUSEIPDB_API_KEY=xxx /usr/local/bin/update-abuseipdb-blocklist.sh
# Block the set
sudo iptables -I INPUT 1 -m set --match-set abuseipdb src -j DROPCron (every 6h, well under free tier 1000/day limit):
0 */6 * * * root ABUSEIPDB_API_KEY=xxx /usr/local/bin/update-abuseipdb-blocklist.sh
Features:
- Atomic swap — no blocking gap during refresh
- Configurable confidence threshold (
--confidence 90, range 75-100) - Up to 10,000 IPs on free tier
The abuse-report action automatically looks up the abuse contact for a banned IP (via RDAP/whois across all registries — RIPE, ARIN, APNIC, LACNIC, AFRINIC) and sends a formatted complaint email with log evidence.
sudo cp action.d/abuse-report.conf /etc/fail2ban/action.d/
sudo cp action.d/fail2ban-abuse-report.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/fail2ban-abuse-report.sh
sudo mkdir -p /var/cache/fail2ban-abuseRequirements: curl, whois, mail (mailutils) or sendmail, jq (recommended) or python3
Add as a second action to any jail in jail.local:
[apache-script-hackbots]
enabled = true
port = http,https
filter = apache-script-hackbots
logpath = /var/log/virtualmin/*access_log
maxretry = 1
findtime = 36000
bantime = 36000
action = iptables-multiport[name=apache-script-hackbots, port="http,https"]
abuse-report[sender=abuse@yourdomain.com, hostname=your-server.com]- Multi-RIR: ARIN RDAP with automatic referrals to RIPE/APNIC/LACNIC/AFRINIC, whois fallback
- Cooldown: same IP reported max once per 24h (configurable)
- Skips private IPs: 10.x, 172.16-31.x, 192.168.x, 127.x
- Evidence: includes up to 50 matching log lines
- Logging: all reports logged to
/var/log/fail2ban-abuse-reports.log
# See what was sent
tail -20 /var/log/fail2ban-abuse-reports.log
# Test abuse lookup for an IP
/usr/local/bin/fail2ban-abuse-report.sh 47.76.209.138 test 5 you@example.com your-server ""The samples/ folder contains real-world attack logs and probe URLs used to build these filters. Useful for testing filters before deployment:
sudo fail2ban-regex samples/url-enumeration-attack.log filter.d/apache-url-enumeration.conf
sudo fail2ban-regex samples/probe-attack-urls.txt filter.d/apache-probe-paths.confSee samples/README.md for details.
These rules are based on real attacks. PRs welcome — especially for:
- New attack patterns you've observed
- Integration with IP reputation lists (AbuseIPDB, Spamhaus DROP, etc.)