Access-log-based filter for wp-login.php brute force detection without requiring the WP fail2ban plugin. Documents the backend=polling gotcha on Ubuntu 24.04 and manual banning workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.7 KiB
title, domain, category, tags, status, created, updated
| title | domain | category | tags | status | created | updated | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Fail2ban Custom Jail: WordPress Login Brute Force | selfhosting | security |
|
published | 2026-04-02 | 2026-04-02 |
Fail2ban Custom Jail: WordPress Login Brute Force
The Problem
WordPress login brute force attacks are extremely common. Bots hammer /wp-login.php with POST requests, cycling through common credentials. The default Fail2ban apache-auth jail doesn't catch these because WordPress returns HTTP 200 on failed logins — not 401 — so nothing appears as an authentication failure in the Apache error log.
There are pre-packaged filters (wordpress-hard.conf, wordpress-soft.conf) that ship with some Fail2ban installations, but these require the WP fail2ban WordPress plugin to be installed. That plugin writes login failures to syslog, which the filters then match. Without the plugin, those filters do nothing.
The Solution
Create a lightweight filter that reads the Apache access log and matches repeated POST requests to wp-login.php directly. No WordPress plugin needed.
Step 1 — Create the filter
Create /etc/fail2ban/filter.d/wordpress-login.conf:
# Fail2Ban filter for WordPress login brute force
# Matches POST requests to wp-login.php in Apache access log
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
ignoreregex =
Step 2 — Add the jail
Add to /etc/fail2ban/jail.local:
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/apache2/access.log
maxretry = 5
findtime = 60
bantime = 30d
backend = polling
5 attempts in 60 seconds is tight enough to catch bots (which fire hundreds of requests per minute) while giving a real human a reasonable margin for typos.
Critical:
backend = pollingis required on Ubuntu 24.04 and other systemd-based distros wherebackend = autodefaults tosystemd. Without it, Fail2ban ignoreslogpathand reads from journald, which Apache doesn't write to. The jail silently monitors nothing. See fail2ban-apache-404-scanner-jail for more detail on this gotcha.
Step 3 — Test the regex
fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wordpress-login.conf
In a real-world test against an active brute force (3 IPs, ~1,700 hits each), this matched 5,178 lines.
Step 4 — Reload and verify
systemctl restart fail2ban
fail2ban-client status wordpress-login
Manually banning known attackers
If you've already identified brute-force IPs from the logs, ban them immediately rather than waiting for new hits:
# Find top offenders
grep "POST /wp-login.php" /var/log/apache2/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
# Ban them
fail2ban-client set wordpress-login banip <IP>
Why Default Jails Miss This
| Jail | Log Source | What It Matches | Why It Misses |
|---|---|---|---|
apache-auth |
error log | 401 authentication failures | WordPress returns 200, not 401 |
wordpress-hard |
syslog | WP fail2ban plugin messages | Requires plugin installation |
wordpress-soft |
syslog | WP fail2ban plugin messages | Requires plugin installation |
wordpress-login |
access log | POST to wp-login.php | No plugin needed |
Optional: Extend to XML-RPC
WordPress's xmlrpc.php is another common brute-force target. To cover both, update the filter:
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /xmlrpc\.php
Quick Diagnostic Commands
# Test filter against current access log
fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wordpress-login.conf
# Check jail status and banned IPs
fail2ban-client status wordpress-login
# Verify the jail is reading the correct file
fail2ban-client get wordpress-login logpath
# Count wp-login POSTs in today's log
grep "POST /wp-login.php" /var/log/apache2/access.log | wc -l
# Watch bans in real time
tail -f /var/log/fail2ban.log | grep wordpress-login
Key Notes
- This filter works with both Apache combined and common log formats.
- Make sure your Tailscale subnet (
100.64.0.0/10) is in theignoreiplist under[DEFAULT]so legitimate admin access isn't banned. - The
recidivejail (if enabled) will escalate repeat offenders — three 30-day bans within a day triggers a 90-day block. - Complements the fail2ban-apache-404-scanner-jail for full access-log coverage.
See Also
- fail2ban-apache-404-scanner-jail — catches vulnerability scanners via 404 floods
- tuning-netdata-web-log-alerts — suppress false Netdata alerts from normal HTTP traffic