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
|
||||
Reference in New Issue
Block a user