Your WordPress login page is getting hammered - hundreds or thousands of POST requests to wp-login.php a day, your security plugin's lockout log is overflowing, and the site feels sluggish even though nobody's actually logged in. A login-limiter plugin helps, but it still lets every one of those requests reach PHP-FPM before it says no. If you're on a VPS, the real fix is blocking the attacker at the firewall with Fail2Ban - before WordPress ever wakes up to handle the request.
Symptom: wp-login.php is getting flooded
You'll usually spot this one of three ways:
- Your access log shows the same handful of IPs (or a rotating botnet) hitting
/wp-login.phpor/xmlrpc.phpevery few seconds, all day long - A plugin like Wordfence or Limit Login Attempts Reloaded is logging dozens of lockouts an hour, but new attempts keep coming from different IPs
- PHP-FPM or MySQL CPU usage stays elevated with no real traffic to explain it, and
topshowsphp-fpmworkers constantly busy
A WordPress-side plugin can lock out a username or an IP, but the request still has to boot WordPress, load the database, and run the plugin's own check first. On a small VPS, a few hundred requests a minute is enough to push load averages up and slow the site for real visitors. Blocking at the firewall with Fail2Ban stops the connection before Nginx or Apache even hands it to PHP.
Cause: WordPress logins don't log failures the way SSH does
If you've already set up Fail2Ban for SSH, you might assume you can point it at wp-login.php the same way. There's a catch: SSH logs a distinct "Failed password" line for every bad attempt, but WordPress's default login page returns an HTTP 200 OK for both a failed and a successful login - the difference is only in the HTML body, not the status code. A naive filter that matches any POST /wp-login.php request will happily ban your own IP the third time you fat-finger your password.
There are two workable approaches:
- Rate-limit the endpoint - ban any IP that hits
wp-login.phporxmlrpc.phpmore times than a real human would in a short window. Blunt, but effective against bot floods, and it needs nothing beyond your existing web server log. - Log real failures - add a small must-use plugin that writes an entry only when WordPress actually rejects a login, then point Fail2Ban at that file. More precise, and it won't touch anyone who just mistypes a password once.
We'll set up the second one, since it's what most support tickets end up needing once the blunt version starts catching real users.
Fix: build a custom Fail2Ban jail for WordPress logins
Step 1: Confirm Fail2Ban is installed
sudo systemctl status fail2ban
fail2ban-client versionIf it's not installed yet, on Ubuntu/Debian:
sudo apt update && sudo apt install fail2ban -y
sudo systemctl enable --now fail2banStep 2: Log failed logins to their own file
Create a must-use plugin so it loads automatically without needing activation:
sudo mkdir -p /var/www/yoursite/wp-content/mu-plugins
sudo nano /var/www/yoursite/wp-content/mu-plugins/log-failed-logins.php <?php
// Logs failed WordPress login attempts for Fail2Ban to watch.
add_action( 'wp_login_failed', function ( $username ) {
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( $_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP ) : false;
$user = sanitize_text_field( $username );
if ( ! $ip ) {
return;
}
$line = sprintf( '[%s] Failed login for user "%s" from %s%s', gmdate( 'Y-m-d H:i:s' ), $user, $ip, PHP_EOL );
error_log( $line, 3, '/var/log/wp-login-fail.log' );
});Create the log file and let the web server user write to it:
sudo touch /var/log/wp-login-fail.log
sudo chown www-data:www-data /var/log/wp-login-fail.log
sudo chmod 640 /var/log/wp-login-fail.logSwap www-data for whatever user your PHP-FPM pool runs as (check ps aux | grep php-fpm if you're not sure).
Step 3: Write the Fail2Ban filter
sudo nano /etc/fail2ban/filter.d/wordpress-auth.conf [Definition]
failregex = Failed login for user ".*" from <HOST>
ignoreregex =Step 4: Create the jail
sudo nano /etc/fail2ban/jail.d/wordpress-auth.conf [wordpress-auth]
enabled = true
filter = wordpress-auth
logpath = /var/log/wp-login-fail.log
port = http,https
maxretry = 5
findtime = 10m
bantime = 1h
action = %(action_)s| Setting | What it controls |
|---|---|
maxretry | How many failed logins from one IP before it's banned |
findtime | The window those failures have to happen in to count |
bantime | How long the IP stays blocked - raise it for repeat offenders |
Step 5: Test the filter before trusting it
Fail2Ban can dry-run a filter against your log without banning anyone, which saves you from finding out about a typo the hard way:
sudo fail2ban-regex /var/log/wp-login-fail.log /etc/fail2ban/filter.d/wordpress-auth.confYou want to see "Success" with a match count greater than zero if you've already got some failed attempts logged. If it shows zero matches, double-check the log file actually has entries and that the regex lines up with the format from Step 2.
Step 6: Restart and verify
sudo systemctl restart fail2ban
sudo fail2ban-client status wordpress-authThat last command shows current bans and total failed attempts for the jail, so you can confirm it's actually catching traffic over the next hour or two.
Whitelist yourself first
Before this goes live, add your own IP (and your team's, and your office's static IP if you have one) to the global ignore list so a bad password on your end doesn't lock you out of your own site:
sudo nano /etc/fail2ban/jail.local [DEFAULT]
ignoreip = 127.0.0.1/8 YOUR.IP.ADDRESS.HEREIf you do get banned mid-setup, unban yourself with sudo fail2ban-client set wordpress-auth unbanip YOUR.IP.ADDRESS.HERE.
What if you're on shared cPanel hosting instead of a VPS?
You won't have root access to install Fail2Ban on shared hosting, and you don't need to - cPanel's built-in cPHulk Brute Force Protection handles this at the account level already. The setup above is specifically for VPS and dedicated servers where you manage the firewall yourself.
Prevention: don't rely on one layer
- Turn on two-factor authentication for every admin account, so a leaked or guessed password alone isn't enough to get in
- Disable XML-RPC if you don't use the WordPress mobile app or Jetpack, since
xmlrpc.phpis a common brute-force shortcut around the normal login form - Keep a login-limiter plugin as a second layer, not your only one - it's a safety net for the handful of requests that slip past the firewall before a ban kicks in
- Check
fail2ban-client status wordpress-authmonthly to see if ban volume is climbing, which is often the first sign a site is being specifically targeted rather than hit by generic bot traffic
None of these replace each other. The firewall jail keeps the noise off your server; 2FA and disabling XML-RPC close off the paths an attacker would use if they ever got past it.