2026年11月14日
2026年11月14日
Protect the WordPress Login Page: Basic Auth, IP Limits, Lockouts, 2FA
This article is a localized edition of the Japanese original. Read the Japanese original
On every WordPress site, the login screen is at /wp-login.php. That makes it an easy target for brute-force attacks that try huge numbers of usernames and passwords, and XML-RPC (/xmlrpc.php) is targeted for the same purpose.
Don't rely on a single measure. Combine them as follows.
| Measure | Effect | Best suited for |
|---|---|---|
| Strong passwords and hard-to-guess usernames | Makes brute force less likely to succeed | Every site |
| Two-factor authentication | Makes logging in hard even if a password leaks | Every site, especially administrators |
| Limiting login attempts | Stops repeated attempts from the same source | Every site |
| Basic authentication | Adds another authentication layer in front of WordPress and stops requests before PHP runs | Sites managed by a small number of people |
| IP address restriction | Allows logins only from fixed locations | When the source IPs are fixed |
| Changing the login URL | Reduces automated traffic (not a substitute for real protection) | As a supplement |
The settings in this article use Apache 2.4 and Nginx syntax. The Apache 2.2 Order/Allow from syntax is not used. Before you start, make sure you have a way to undo settings if you get locked out, such as FTP, SSH or your server's control panel.
Step 1: Put Basic authentication on the login screen
Basic authentication adds another gate in front of the WordPress login screen, using the username and password prompt the browser shows. Requests that fail it never reach PHP, which also reduces server load. Basic authentication is not encrypted by itself, so the site must already use HTTPS.
Create the password file
Keep the password file outside the public directory (document root). htpasswd comes in apache2-utils on Ubuntu/Debian and in httpd-tools on RHEL-based systems.
# Create a new file (-c overwrites an existing file, so omit it for additional users)
sudo htpasswd -c -B /etc/apache2/.htpasswd admin_basic
# Add another user
sudo htpasswd -B /etc/apache2/.htpasswd another_user
-B stores the password with bcrypt. For details, see Creating an htpasswd file.
Apache 2.4: protect wp-login.php
Add this to the .htaccess in the directory where WordPress is installed (the same place as wp-login.php). To configure authentication in .htaccess, the VirtualHost must allow AllowOverride AuthConfig (or All).
<Files "wp-login.php">
AuthType Basic
AuthName "Login"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
</Files>
The part WordPress writes between # BEGIN WordPress and # END WordPress can be rewritten, for example when you save the permalink settings, so put your rules outside it.
Apache 2.4: protect wp-admin
<Directory> can't be used inside .htaccess. Create a separate .htaccess inside the wp-admin directory.
# wp-admin/.htaccess
AuthType Basic
AuthName "Admin"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
# Leave admin-ajax.php, used by front-end features, outside authentication
<Files "admin-ajax.php">
Require all granted
</Files>
wp-admin/admin-ajax.php is also used by features for visitors who aren't logged in (forms, "like" buttons and so on). If you leave it behind authentication, those features stop working.
Caveats of Basic authentication
- Password-protected posts and pages: when a visitor submits the password, it is sent to
wp-login.php?action=postpass. Ifwp-login.phpis behind Basic authentication, visitors see the Basic authentication prompt too. On sites that use password-protected pages, choose IP restriction or attempt limits instead. - Registration and password resets: these also use
wp-login.php, so this approach doesn't suit sites where users register.
For Nginx, see Basic authentication on Nginx.
Step 2: Restrict logins by IP address
If your home or office IP address is fixed, you can allow wp-login.php to open only from that IP. If anyone works from a mobile connection or a line whose IP changes, choose another method.
Apache 2.4
Write this in .htaccess. You can list IP addresses and networks (CIDR) separated by spaces. Put comments on their own lines, not at the end of a line (Apache does not accept trailing comments).
# Example: 203.0.113.10 is the office, 198.51.100.0/24 is the VPN
<Files "wp-login.php">
Require ip 203.0.113.10 198.51.100.0/24
</Files>
To restrict wp-admin as well, write Require ip in wp-admin/.htaccess as in Step 1, and exclude admin-ajax.php with Require all granted.
Nginx
In Nginx, create location = /wp-login.php and include the PHP handling settings inside it too. If you write only allow/deny, even allowed IPs may get the file downloaded or not executed as PHP.
location = /wp-login.php {
allow 203.0.113.10;
allow 198.51.100.0/24;
deny all;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
The fastcgi_pass socket varies by environment, so use the same one as your existing location ~ \.php$. After the change, check with sudo nginx -t and then apply it with sudo systemctl reload nginx.
Behind a proxy or CDN
If traffic passes through a CDN such as Cloudflare or a load balancer, the source the server sees is the CDN's IP, and IP restriction doesn't work as intended. Restore the visitor IP on the web server first (Step 3 of Cloudflare CDN setup).
For details, also see Restricting the login page by IP.
Step 3: Limit login attempts
WordPress core has no feature that counts failed logins and blocks them. Use a plugin (such as Limit Login Attempts Reloaded) or put code like the following in a custom plugin or your theme's functions.php.
This code blocks logins from an IP address for 30 minutes after 5 failures from that IP within 30 minutes. Because it counts per IP rather than per username, it also works against attacks that keep changing usernames. Authentication via XML-RPC and application passwords goes through the same authenticate filter, so those attempts are counted too.
<?php
/**
* Count failed logins per IP address and block logins for a while once the limit is reached.
*/
const MYSITE_LOGIN_MAX_FAILS = 5;
const MYSITE_LOGIN_LOCK_SEC = 30 * MINUTE_IN_SECONDS;
function mysite_login_fail_key(): string {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return 'mysite_login_fail_' . md5( $ip );
}
// After authentication (priority 20), replace the result with an error if the limit is reached.
add_filter( 'authenticate', function ( $user, $username, $password ) {
if ( '' === (string) $username ) {
return $user;
}
$fails = (int) get_transient( mysite_login_fail_key() );
if ( $fails >= MYSITE_LOGIN_MAX_FAILS ) {
return new WP_Error(
'too_many_retries',
sprintf(
'Too many failed login attempts. Login is temporarily blocked. Please try again in about %d minutes.',
MYSITE_LOGIN_LOCK_SEC / MINUTE_IN_SECONDS
)
);
}
return $user;
}, 30, 3 );
// Increase the count on failure.
add_action( 'wp_login_failed', function ( string $username ): void {
$key = mysite_login_fail_key();
$fails = (int) get_transient( $key );
set_transient( $key, $fails + 1, MYSITE_LOGIN_LOCK_SEC );
} );
// Clear the count on success.
add_action( 'wp_login', function ( string $user_login ): void {
delete_transient( mysite_login_fail_key() );
} );
- Attempts made while blocked also count as failures, and the block is extended by 30 minutes from that point.
- Because it uses
REMOTE_ADDR, you need to restore visitor IPs behind a CDN or proxy. Otherwise everyone is counted as the same IP, and a single attacker can lock everyone else out. - If you lock yourself out, wait 30 minutes or run
wp transient delete --allwith WP-CLI (this also deletes other transients, but the ones that are needed are regenerated).
For other approaches, see Limiting login attempts without a plugin and Limiting login attempts.
Step 4: Enable two-factor authentication
With two-factor authentication, logging in requires a verification code, such as one shown by an authenticator app on your phone, in addition to the password. A leaked password alone isn't enough to log in.
With the "Two-Factor" plugin developed by WordPress contributors (version 0.17.0 as of November 2026, requiring WordPress 7.0 or later and PHP 7.4 or later):
- Install and activate Two-Factor under "Plugins" → "Add New Plugin".
- In "Two-Factor Options" under "Users" → "Profile", choose the method to use. For an authenticator app (TOTP), scan the QR code shown with an app such as Google Authenticator or 1Password, then enter the code it displays to confirm.
- In case you lose your phone, also generate backup codes and keep them somewhere safe.
You can also choose to receive verification codes by email. Each user sets this up in their own profile (administrators can also edit other users' profiles). To require two-factor authentication for everyone or only for administrators, use a plugin with enforcement features such as WP 2FA (How to enforce two-factor authentication).
For detailed steps, see Setting up two-factor authentication in WordPress.
Step 5: Change the login URL (as a supplement)
Changing the login URL reduces automated traffic aimed at /wp-login.php. But once the URL is known it does nothing, so it is not a substitute for the measures above.
Hiding wp-login.php with your own code easily breaks logout, password resets, password-protected pages and more, so use a plugin. With WPS Hide Login, after activating it, choose the new login slug under "Settings" → "WPS Hide Login". If you use a page caching plugin, add the new login URL to its cache exclusions (the official page says WP Rocket is already compatible).
Step 6: Disable XML-RPC if you don’t use it
xmlrpc.php is an old mechanism for posting from external apps and services, and like the login screen it is used for password brute force. If nothing uses XML-RPC, such as Jetpack or some mobile apps, you can turn it off. See How to disable XML-RPC.
Checking the logs for attacks
When a login fails, WordPress redisplays the login screen with status code 200, and on success it redirects with 302. So counting 403 or 429 responses tells you nothing about failures. Count POSTs to wp-login.php and xmlrpc.php per source.
# Client IPs sending the most POSTs to wp-login.php (Nginx default log format)
grep 'POST /wp-login.php' /var/log/nginx/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# POSTs to xmlrpc.php
grep -c 'POST /xmlrpc.php' /var/log/nginx/access.log
For Apache, change the log path to /var/log/apache2/access.log (/var/log/httpd/access_log on RHEL-based systems). If large numbers of POSTs keep coming from the same IP, consider blocking it with a firewall or fail2ban in addition to the measures above.
Cautions
- Before adding an IP restriction, confirm that your own IP is on the allow list. If you get it wrong, fix
.htaccessover FTP or SSH. - Changing the login URL and limiting attempts do little to reduce overall server load. When attacks are heavy, methods that stop requests before PHP runs, such as Basic authentication or IP restriction, work better.
- If an administrator uses an easy-to-guess username such as
admin, create a new administrator user and then delete the old one (attributing its posts to the new user).
Summary
- Two-factor authentication and login attempt limits are worth having on every site.
- If only a few people manage the site, add Basic authentication; if the source IPs are fixed, add IP restriction, stopping requests before PHP runs. Write the settings with Apache 2.4
Requireor Nginxallow/deny. - When you put authentication on
wp-admin, excludeadmin-ajax.phpand check the effect on password-protected pages and registration. - Check for attacks by counting POSTs to
wp-login.phpandxmlrpc.php.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)