--- title: "Fail2ban Custom Jail: WordPress Login Brute Force" domain: selfhosting category: security tags: [fail2ban, wordpress, apache, security, brute-force] status: published created: 2026-04-02 updated: 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](https://wordpress.org/plugins/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`: ```ini # Fail2Ban filter for WordPress login brute force # Matches POST requests to wp-login.php in Apache access log [Definition] failregex = ^ .* "POST /wp-login\.php ignoreregex = ``` ### Step 2 — Add the jail Add to `/etc/fail2ban/jail.local`: ```ini [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 = polling` is required** on Ubuntu 24.04 and other systemd-based distros where `backend = auto` defaults to `systemd`. Without it, Fail2ban ignores `logpath` and 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 ```bash 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 ```bash 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: ```bash # 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 ``` ## 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: ```ini failregex = ^ .* "POST /wp-login\.php ^ .* "POST /xmlrpc\.php ``` ## Quick Diagnostic Commands ```bash # 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 the `ignoreip` list under `[DEFAULT]` so legitimate admin access isn't banned. - The `recidive` jail (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|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