Files
MajorWiki/02-selfhosting/security/fail2ban-wordpress-login-jail.md
MajorLinux 9a7e43e67d 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>
2026-04-02 16:04:13 -04:00

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
fail2ban
wordpress
apache
security
brute-force
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 = 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

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 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 for full access-log coverage.

See Also