2026年11月12日
2026年11月12日
WordPress Brute-Force Defense: Login Limits, IP Rules, 2FA, Fail2ban
This article is a localized edition of the Japanese original. Read the Japanese original
A brute-force attack automatically tries huge numbers of username and password combinations. WordPress core has no built-in limit on failed logins, so without any countermeasures the attempts simply keep coming.
Rather than relying on a single measure, the standard approach is to stack several layers, as shown below.
| Layer | Measure | Main effect |
|---|---|---|
| Account | Hard-to-guess passwords, two-factor authentication | Blocks the login even if the password is guessed |
| WordPress | Limit login attempts, disable XML-RPC | Stops repeated attempts from the same IP |
| Web server | IP restriction, Basic authentication and rate limiting for wp-login.php | Stops requests before they reach PHP and reduces load |
| OS | Fail2ban | Blocks IPs that keep failing at the firewall |
Check whether you are under attack
Count POST requests to wp-login.php and xmlrpc.php in the access log. The path below is for Apache on Ubuntu; for Nginx use /var/log/nginx/access.log.
# Top IPs sending POST requests to wp-login.php
grep '"POST /wp-login.php' /var/log/apache2/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# Also check POST requests to xmlrpc.php
grep -c '"POST /xmlrpc.php' /var/log/apache2/access.log
When a login fails, WordPress shows the login screen again, so the status is 200. A successful login redirects, so the status is 302. If you see lots of POST requests with status 200 from the same IP, you are being brute-forced.
Limit login attempts
Limit with a plugin
If you want to configure everything from the admin, use a login-limiting plugin. One example is Limit Login Attempts Reloaded (more than 1 million active installs as of November 2026).
wp plugin install limit-login-attempts-reloaded --activate
To install it from the admin, go to Plugins (プラグイン) → Add New (新規追加) and search for its name. Security plugins such as Wordfence have a similar feature, so if you already use one, check its settings first. There's no need to stack several plugins that do the same thing.
Limit with code
If you'd rather not add another plugin, save the following code as wp-content/mu-plugins/login-limit.php (create the mu-plugins directory if it doesn't exist). After 5 failures from the same IP, logins are refused for 15 minutes.
<?php
/**
* Plugin Name: Simple Login Limit
* Description: Blocks logins for a while after repeated failures from the same IP.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const SLL_MAX_FAILS = 5;
const SLL_LOCK_MINUTES = 15;
function sll_key(): string {
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '';
return 'sll_fail_' . md5( $ip );
}
// At the end of authentication (core checks the password at priority 20), reject if the limit is exceeded
add_filter(
'authenticate',
function ( $user, $username ) {
if ( '' === (string) $username ) {
return $user;
}
if ( (int) get_transient( sll_key() ) >= SLL_MAX_FAILS ) {
return new WP_Error(
'sll_locked',
sprintf( 'Too many failed login attempts. Please try again in %d minutes.', SLL_LOCK_MINUTES )
);
}
return $user;
},
30,
2
);
// Increment the counter on failure
add_action(
'wp_login_failed',
function () {
$fails = (int) get_transient( sll_key() );
set_transient( sll_key(), $fails + 1, SLL_LOCK_MINUTES * MINUTE_IN_SECONDS );
}
);
// Clear the counter on success
add_action(
'wp_login',
function () {
delete_transient( sll_key() );
}
);
- The counter is stored in a transient. If you use a persistent object cache (such as Redis), it lives there, so flushing the whole cache also resets the counter.
- Behind Cloudflare or a load balancer,
REMOTE_ADDRbecomes the proxy's IP and everyone gets blocked together. In that case, don't trust headers such asHTTP_CF_CONNECTING_IPdirectly in PHP (they can be spoofed); instead use Apache'smod_remoteipor Nginx'sreal_ipmodule to replace the address with the real client IP only for connections from proxies you trust.
Limiting logins with code is covered in more detail in How to limit login attempts without a plugin.
Restrict access to wp-login.php at the web server
Stopping requests before they reach PHP also reduces server load. Before changing these settings, make sure you can undo them by some means other than the web, such as SSH or your provider's control panel.
Apache 2.4: restrict by IP address
If you only manage the site from fixed IPs, add the following to .htaccess (or the VirtualHost configuration).
<Files "wp-login.php">
Require ip 203.0.113.10
Require ip 198.51.100.0/24
</Files>
To use Require in .htaccess, the directory's AllowOverride must include AuthConfig (or All). Order Deny,Allow / Allow from is Apache 2.2 syntax. On Apache 2.4 it causes an error when mod_access_compat is disabled, so use Require ip.
Apache 2.4: add Basic authentication
If your IP isn't fixed, add Basic authentication to wp-login.php. That puts one more gate in front of the WordPress password.
sudo apt install apache2-utils
sudo htpasswd -c /etc/apache2/.htpasswd-wplogin loginadmin
<Files "wp-login.php">
AuthType Basic
AuthName "Restricted"
AuthUserFile /etc/apache2/.htpasswd-wplogin
Require valid-user
</Files>
-c creates a new file. When you add a second user or more, run the command without -c (with it, the file is overwritten). The password reset screen is also wp-login.php, so this method doesn't suit sites where ordinary visitors log in, such as membership sites.
Nginx: rate limiting and IP restriction
In Nginx, limit_req limits how fast requests can come from the same IP. Put limit_req_zone in the http block.
# Inside http { } in /etc/nginx/nginx.conf
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=6r/m;
# Inside the site's server { } block
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
limit_req_status 429;
# To allow only fixed IPs
# allow 203.0.113.10;
# deny all;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
rate=6r/mmeans 6 requests per minute (one every 10 seconds), andburst=5allows up to 5 extra requests.snippets/fastcgi-php.confis included in Ubuntu's nginx package. The socket path depends on the PHP version, so check it withls /run/php/and adjust.- After editing, check the syntax with
sudo nginx -tand apply the change withsudo systemctl reload nginx.
Disable XML-RPC
xmlrpc.php is an entry point where a username and password can be sent without going through the login screen. If you don't use anything that relies on XML-RPC, such as the mobile apps or Jetpack, turn it off.
To disable only the XML-RPC methods that require authentication, use the core filter xmlrpc_enabled (features that need no authentication, such as pingbacks, keep working).
<?php
// wp-content/mu-plugins/disable-xmlrpc-auth.php
add_filter( 'xmlrpc_enabled', '__return_false' );
To block the file entirely, deny it at the web server (Apache 2.4 example).
<Files "xmlrpc.php">
Require all denied
</Files>
Add two-factor authentication
Even if a password is guessed, two-factor authentication (2FA) prevents the login. Options include Two Factor, an official WordPress feature plugin, and WP 2FA, which can require 2FA by role.
wp plugin install two-factor --activate
How to make administrators and editors set up 2FA is explained in How to enforce two-factor authentication (2FA) in WordPress.
Block IPs that keep failing with Fail2ban
Fail2ban watches logs and blocks matching IPs at the firewall for a set time. It doesn't ship with a WordPress filter, so you create one.
sudo apt install fail2ban
Create the filter /etc/fail2ban/filter.d/wordpress-login.conf. It counts only POST requests to wp-login.php with status 200, which indicates a failure.
[Definition]
failregex = ^<HOST> \S+ \S+ \[[^\]]*\] "POST /wp-login\.php[^"]*" 200\s
ignoreregex =
It assumes the standard Apache/Nginx log format (combined). You can check whether it matches your log with fail2ban-regex.
sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wordpress-login.conf
If a count appears in Lines: ... matched in the output, the filter matches. Next, create the jail in /etc/fail2ban/jail.d/wordpress-login.local.
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/apache2/access.log
backend = auto
maxretry = 5
findtime = 10m
bantime = 1h
backend = auto is there so that the file in logpath is monitored even if the distribution's defaults read the systemd journal instead.
sudo systemctl restart fail2ban
sudo fail2ban-client status wordpress-login
Blocked IPs appear under Banned IP list. If you block your own IP by mistake, unblock it with sudo fail2ban-client set wordpress-login unbanip 203.0.113.10.
Things to watch out for
- IP restrictions, Basic authentication and enforced 2FA can lock you out if misconfigured. Make sure you can restore files over SSH before making changes.
- If traffic passes through a proxy such as Cloudflare, the IP seen by the web server logs and Fail2ban is the proxy's. You first need to log the real client IP with
mod_remoteipor similar, as described above. Also, even when Fail2ban blocks an IP at the server firewall, traffic coming through the proxy is not stopped. - Attackers often rotate through many IPs, so blocking IPs alone won't stop them. Build on hard-to-guess passwords and 2FA.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)