Fail2Ban + ModSecurity
Cloudflare WAF is your first line of defense. Fail2Ban and ModSecurity are the origin-side safety net: they help when traffic bypasses the CDN (direct IP, misconfigured DNS, internal tooling) or when you need extra enforcement close to the server.
When to Use Which
| Tool | Best for | What it does | Where it runs |
|---|---|---|---|
| Fail2Ban | brute force + repeated abuse patterns | bans IPs based on log patterns | OS firewall layer |
| ModSecurity | request payload attacks (SQLi/XSS/RCE patterns) | inspects and blocks requests | web server request path |
Fail2Ban does not run in the hot path of requests (it reacts to logs). ModSecurity does run in the request path, so treat it as a change you must validate and measure.
Fail2Ban
If you are behind Cloudflare, make sure your web server logs the real visitor IP (via CF-Connecting-IP / X-Forwarded-For). If your logs only show Cloudflare IPs, Fail2Ban will ban the CDN, not the attacker.
Install (Debian/Ubuntu)
sudo apt update
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
Minimal Configuration
Create a local override (do not edit the packaged defaults):
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Always whitelist your own admin IPs (or your VPN egress)
ignoreip = 127.0.0.1/8 ::1 YOUR_ADMIN_IP
[sshd]
enabled = true
maxretry = 3
bantime = 24h
# WordPress (adjust logpath for your stack)
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /usr/local/lsws/logs/access.log
maxretry = 5
findtime = 5m
bantime = 1h
[wordpress-xmlrpc]
enabled = true
port = http,https
filter = wordpress-xmlrpc
logpath = /usr/local/lsws/logs/access.log
maxretry = 2
bantime = 24h
If you use Nginx/Apache, your access log path will differ (for example /var/log/nginx/access.log). Keep the filters the same, but point logpath at the correct file.
Filters
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
ignoreregex =
[Definition]
failregex = ^<HOST> .* "POST /xmlrpc\.php
ignoreregex =
Verify and Operate
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client status wordpress-login
sudo fail2ban-regex /usr/local/lsws/logs/access.log /etc/fail2ban/filter.d/wordpress-login.conf
sudo fail2ban-client set wordpress-login unbanip 1.2.3.4
ModSecurity (Optional)
ModSecurity is a request-inspection WAF at the origin. It can catch payload-style attacks that slip past edge rules, but it can also create false positives (especially in wp-admin and REST traffic).
Start in Detection-Only
SecRuleEngine DetectionOnly
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecAuditEngine RelevantOnly
After you review audit logs and add exclusions, move to blocking mode:
SecRuleEngine On
Use OWASP Core Rule Set (CRS)
Install CRS, then include it from your ModSecurity config (paths vary by stack). Keep response-body inspection off unless you have a specific reason.
WordPress-Specific Exclusions
Start small and only exclude what you can justify via logs:
# REST API (many editors/plugins rely on it)
SecRule REQUEST_URI "@beginsWith /wp-json/" \
"id:1002,phase:1,nolog,pass,ctl:ruleRemoveById=920420"
# Admin AJAX
SecRule REQUEST_URI "@beginsWith /wp-admin/admin-ajax.php" \
"id:1001,phase:1,nolog,pass,ctl:ruleRemoveById=942100"
Avoid running multiple full WAF layers that all inspect payloads (edge WAF + origin ModSecurity + heavy plugin WAF). Pick one primary inspection layer (edge preferred) and keep the others minimal.
Common Pitfalls
| Pitfall | What it looks like | Fix |
|---|---|---|
| Fail2Ban bans Cloudflare | large parts of the site go offline | log real visitor IPs before enabling HTTP jails |
No ignoreip | you lock yourself out | whitelist your admin/VPN IPs |
| ModSecurity blocks wp-admin/editor | saves/publishes fail, REST errors | start in DetectionOnly, then add narrow exclusions |
| Treating ModSecurity as "set and forget" | random breakage after CRS updates | review audit logs after updates and retest critical flows |