Skip to main content

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

ToolBest forWhat it doesWhere it runs
Fail2Banbrute force + repeated abuse patternsbans IPs based on log patternsOS firewall layer
ModSecurityrequest payload attacks (SQLi/XSS/RCE patterns)inspects and blocks requestsweb server request path
note

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

caution

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)

Install Fail2Ban
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):

/etc/fail2ban/jail.local
[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
tip

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

/etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
ignoreregex =
/etc/fail2ban/filter.d/wordpress-xmlrpc.conf
[Definition]
failregex = ^<HOST> .* "POST /xmlrpc\.php
ignoreregex =

Verify and Operate

Check Fail2Ban and jail status
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client status wordpress-login
Test a filter against a real log file
sudo fail2ban-regex /usr/local/lsws/logs/access.log /etc/fail2ban/filter.d/wordpress-login.conf
Unban an IP
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

modsecurity.conf (safe starting point)
SecRuleEngine DetectionOnly
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecAuditEngine RelevantOnly

After you review audit logs and add exclusions, move to blocking mode:

modsecurity.conf (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:

wordpress-modsecurity-exclusions.conf
# 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"
caution

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

PitfallWhat it looks likeFix
Fail2Ban bans Cloudflarelarge parts of the site go offlinelog real visitor IPs before enabling HTTP jails
No ignoreipyou lock yourself outwhitelist your admin/VPN IPs
ModSecurity blocks wp-admin/editorsaves/publishes fail, REST errorsstart in DetectionOnly, then add narrow exclusions
Treating ModSecurity as "set and forget"random breakage after CRS updatesreview audit logs after updates and retest critical flows

What's Next