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:
131
02-selfhosting/security/fail2ban-wordpress-login-jail.md
Normal file
131
02-selfhosting/security/fail2ban-wordpress-login-jail.md
Normal 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
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
* [Linux Server Hardening Checklist](02-selfhosting/security/linux-server-hardening-checklist.md)
|
* [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)
|
* [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: 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)
|
* [SELinux: Fixing Fail2ban grep execmem Denial](02-selfhosting/security/selinux-fail2ban-execmem-fix.md)
|
||||||
* [UFW Firewall Management](02-selfhosting/security/ufw-firewall-management.md)
|
* [UFW Firewall Management](02-selfhosting/security/ufw-firewall-management.md)
|
||||||
* [Open Source & Alternatives](03-opensource/index.md)
|
* [Open Source & Alternatives](03-opensource/index.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user