Add wiki article: Fail2ban WordPress login brute force jail

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>
This commit is contained in:
2026-04-02 16:04:13 -04:00
parent 87d63039af
commit 1cf70f9b58
2 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
---
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 = ^<HOST> .* "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 <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:
```ini
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "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

View File

@@ -31,6 +31,7 @@
* [Linux Server Hardening Checklist](02-selfhosting/security/linux-server-hardening-checklist.md)
* [Standardizing unattended-upgrades with Ansible](02-selfhosting/security/ansible-unattended-upgrades-fleet.md)
* [Fail2ban Custom Jail: Apache 404 Scanner Detection](02-selfhosting/security/fail2ban-apache-404-scanner-jail.md)
* [Fail2ban Custom Jail: WordPress Login Brute Force](02-selfhosting/security/fail2ban-wordpress-login-jail.md)
* [SELinux: Fixing Fail2ban grep execmem Denial](02-selfhosting/security/selinux-fail2ban-execmem-fix.md)
* [UFW Firewall Management](02-selfhosting/security/ufw-firewall-management.md)
* [Open Source & Alternatives](03-opensource/index.md)