# Protect WordPress with fail2ban

> Protect WordPress with fail2ban: the WP fail2ban plugin and its wordpress-hard/soft filters, or a plugin-free access-log filter for wp-login.php and xmlrpc.php.
>
> Source: https://fail2ban.dev/guides/wordpress/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

To protect WordPress with fail2ban, either install the **WP fail2ban** plugin, copy its `wordpress-hard.conf` and `wordpress-soft.conf` filters to `/etc/fail2ban/filter.d/` and enable two jails reading `/var/log/auth.log`, or — without a plugin — add a filter that counts `POST /wp-login.php` requests answered with `200` (a failed login; a successful one redirects with `302`) and `POST /xmlrpc.php` requests in your nginx or Apache access log.

```ini
[wordpress-hard]
enabled  = true
backend  = auto
filter   = wordpress-hard
logpath  = /var/log/auth.log
port     = http,https
maxretry = 1

[wordpress-soft]
enabled  = true
backend  = auto
filter   = wordpress-soft
logpath  = /var/log/auth.log
port     = http,https
maxretry = 3
```

## Option 1: the WP fail2ban plugin

**WP fail2ban – Advanced Security** (plugin slug `wp-fail2ban`, by Charles Lecklider) writes WordPress security events to syslog: failed logins, logins for unknown users, XML-RPC authentication attempts, blocked user enumeration, pingbacks and more. Version 5.5.0 was current when we checked the plugin directory. Install it from the dashboard or with WP-CLI:

```bash
```

The plugin ships its filters in its `filters.d` directory. Copy them into fail2ban:

```bash
        /var/www/html/wp-content/plugins/wp-fail2ban/filters.d/wordpress-soft.conf \
        /etc/fail2ban/filter.d/
```
|---|---|---|
| `wordpress-hard` | Authentication attempt for an unknown user via XML-RPC or REST, XML-RPC multicall failures, blocked user enumeration, pingback errors | 1 |
| `wordpress-soft` | Wrong password for an existing user, empty username, login attempt for an unknown user | 3–5 |

The SVN copy of the plugin also contains a `wordpress-extra.conf` for password-reset requests and comments; the plugin's own readme documents only the hard and soft filters, so start with those. Recopy the filters after plugin upgrades — the readme notes when they change.

### Where the plugin logs

The plugin logs through PHP's `syslog()` with the identifier `wordpress` to the `auth` facility. With rsyslog, that ends up in `/var/log/auth.log` (Debian/Ubuntu) or `/var/log/secure` (RHEL/Fedora):

```log
```

On systems **without rsyslog** (Debian 12 and later, many minimal images), the messages only exist in the systemd journal. Read them there:

```ini
[wordpress-soft]
enabled      = true
backend      = systemd
journalmatch = SYSLOG_IDENTIFIER=wordpress
filter       = wordpress-soft
port         = http,https
maxretry     = 3
```

### WordPress behind a proxy or CDN

If WordPress sits behind a reverse proxy, a load balancer or Cloudflare, PHP sees the proxy's address, and the plugin logs that. Tell the plugin which proxies to trust in `wp-config.php`:

```php
define('WP_FAIL2BAN_PROXIES', ['10.0.0.5', '192.168.1.0/24']);
```

## Option 2: plugin-free filter on the access log

WordPress answers a **failed** login form submission with `200` (the form is shown again with an error) and a **successful** one with a `302` redirect. Counting `POST /wp-login.php … 200` in the access log therefore counts failed logins. `xmlrpc.php` is the other brute-force target: one request can try many passwords via `system.multicall`, so any POST to it from a stranger is suspicious.

```ini
[Definition]
failregex = ^<HOST> \S+ \S+ \[\] "POST /wp-login\.php[^"]*" 200\s
            ^<HOST> \S+ \S+ \[\] "POST /xmlrpc\.php[^"]*" \d+\s
datepattern = ^[^\[]*\[({DATE})
              {^LN-BEG}
```

```log
192.0.2.15 - - [25/Oct/2026:18:36:12 +0000] "POST /xmlrpc.php HTTP/1.1" 200 403 "-" "Mozilla/5.0"
```

### Jail for nginx

```ini
[wordpress-login]
enabled  = true
backend  = auto
filter   = wordpress-login
logpath  = /var/log/nginx/access.log
port     = http,https
maxretry = 5
findtime = 10m
bantime  = 1h
```

```ini
[wordpress-login]
enabled  = true
backend  = auto
filter   = wordpress-login
logpath  = %(apache_access_log)s
port     = http,https
maxretry = 5
findtime = 10m
bantime  = 1h
```

Limitations of the plugin-free method: password-reset submissions (`wp-login.php?action=lostpassword`) with a typo also return 200, and security plugins that change the login URL move the target. The plugin knows what actually happened; the access log only knows status codes.

## Block xmlrpc.php if you don't need it

Most sites don't use XML-RPC (the mobile apps and Jetpack can). If you don't, deny it at the web server and let fail2ban ban those who keep trying.

```nginx
location = /xmlrpc.php {
    deny all;
}
```
<Files "xmlrpc.php">
    Require all denied
</Files>
```

## Test it

```bash
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/wordpress-soft.conf

# plugin-free route
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-login.conf

sudo fail2ban-client status wordpress-soft
```

## FAQ

### Does fail2ban have a WordPress filter?

No. fail2ban 1.1.1 doesn't ship one. The WP fail2ban plugin provides `wordpress-hard.conf` and `wordpress-soft.conf`, which match the events it logs to syslog. Without the plugin, write a filter that counts `POST /wp-login.php` requests with status 200 and `POST /xmlrpc.php` requests in the access log.

### How does fail2ban detect failed WordPress logins without a plugin?

WordPress redisplays the login form with status 200 when a login fails and redirects with 302 when it succeeds. A filter matching `"POST /wp-login.php … " 200` in the nginx or Apache access log therefore counts failed attempts. Successful logins and simple page views of the login form aren't counted.

### Should I ban xmlrpc.php requests?

If you don't use XML-RPC (WordPress mobile apps, Jetpack, some publishing tools), yes: block `xmlrpc.php` in the web server and ban repeat offenders. Attackers use `system.multicall` to try many passwords in a single request, so a low `maxretry` is appropriate for POSTs to it.

### Where does the WP fail2ban plugin write its log?

To syslog with the identifier `wordpress` and the auth facility, so normally `/var/log/auth.log` on Debian/Ubuntu or `/var/log/secure` on RHEL. Without rsyslog it's only in the systemd journal; use `backend = systemd` and `journalmatch = SYSLOG_IDENTIFIER=wordpress` in the jails.

### Why does the WP fail2ban plugin log my proxy's IP?

Because PHP sees the connection from the reverse proxy or CDN. Define `WP_FAIL2BAN_PROXIES` in `wp-config.php` with the proxies' IPs or CIDR ranges; the plugin then reads the client address from `X-Forwarded-For`. Untrusted `X-Forwarded-For` headers are logged as a hard failure instead.
