Stop Brute-Force Attacks on Your WordPress Login

Every public WordPress site gets login attempts from bots. They try common usernames and leaked passwords against wp-login.php and xmlrpc.php, thousands of times a day. Most fail. The risk is the one that succeeds, and the load that all the failed ones put on your server in the meantime.
No single setting stops this. What works is a few layers: accounts that are hard to guess, a second factor, limits on how fast anyone can try, and blocking entry points you do not use. This guide covers each layer, from the dashboard to the server.
Step 1: See what is hitting your site
Before you change anything, look at your access log. On a VPS with Nginx it is usually /var/log/nginx/access.log. On shared hosting, the control panel has a raw access log you can download.
# How many login and XML-RPC POSTs are in the log?
grep -cE '"POST /(wp-login|xmlrpc)\.php' /var/log/nginx/access.log
# Which IP addresses send the most?
awk '$6 == "\"POST" && $7 ~ /wp-login\.php|xmlrpc\.php/ {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
A few dozen POSTs a day is normal background noise. Hundreds per hour from a short list of IPs is a targeted attempt. Thousands per hour spread across many IPs is a distributed attack, and that is where server-level limits matter most.
Why this costs you even when the attacks fail: each login attempt runs PHP and queries the database. On a small plan, enough attempts can slow the site or exhaust PHP workers, so real visitors wait.
Step 2: Make the accounts hard to break
Strong, unique passwords for every account with access
Bots do not guess randomly. They use lists of passwords leaked from other sites. A password reused from another service is the most common way in. Every administrator, editor and shop manager needs a long, unique password from a password manager.
# List users with admin rights
wp user list --role=administrator --fields=ID,user_login,user_email
# Force a new strong password for one account
wp user update 1 --user_pass="$(openssl rand -base64 24)"
No obvious usernames
Avoid admin, the site name or the domain as a username. WordPress does not let you rename a user, so create a new administrator with a different username, log in as it, then delete the old account and assign its content to the new one. Keep in mind that usernames can often be found through author archives and the REST API, so a hidden username helps a little but is not a defence on its own. The password and the second factor are what count.
Fewer administrators
Give the Administrator role only to people who need it. Remove old accounts for staff, agencies and freelancers who no longer work on the site.
Step 3: Turn on two-factor authentication
Two-factor authentication (2FA) means a correct password is not enough. The user also needs a one-time code from an authenticator app. This defeats password guessing and leaked-password attacks, because the bot does not have the phone.
Use an authenticator app (TOTP) rather than SMS where you can.
Require 2FA for every role that can publish, install plugins or see orders.
Store the backup codes somewhere safe, so a lost phone does not lock you out.
Several well-maintained plugins add 2FA to the standard WordPress login, including the Two-Factor plugin maintained by WordPress contributors. Choose one that is updated often and works with your login page and any WooCommerce account page.
Step 4: Limit login attempts
In WordPress
A login-limit or security plugin can lock out an IP after a few failed attempts. This is easy to set up on any hosting plan. The limit is that WordPress and PHP still run for each attempt before the plugin can reject it, so it protects the accounts but does little for server load.
At the web server (Nginx)
Rate limiting in Nginx rejects excess requests before PHP starts. This rule allows about five login requests per minute per IP, with a small burst for real users who mistype a password. Treat the numbers as a starting point and adjust them for your site.
# In the http { } block, for example in /etc/nginx/nginx.conf
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=5r/m;
# In your site's server { } block
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
limit_req_status 429;
# Keep the same PHP lines your main "location ~ \.php$" block uses, e.g.:
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Test the configuration with sudo nginx -t and reload with sudo systemctl reload nginx. Requests over the limit get HTTP 429 instead of a login page.
With fail2ban
fail2ban reads your logs and blocks IPs at the firewall after repeated hits. This filter counts POSTs to the login and XML-RPC files, so it also counts successful logins. With ten allowed in ten minutes, real users are rarely affected.
# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /xmlrpc\.php
ignoreregex =
# /etc/fail2ban/jail.local
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 600
bantime = 3600
sudo systemctl restart fail2ban
sudo fail2ban-client status wordpress-login
If your site sits behind a CDN or reverse proxy, the log may show the proxy's IP instead of the visitor's. Configure the real client IP first, or you will block the proxy and take your site offline.
Step 5: Close XML-RPC if you do not use it
xmlrpc.php is an older remote API. Its system.multicall method lets an attacker try many passwords in a single request, which slips past simple per-request limits. Some tools still need it, such as certain mobile publishing apps, older Jetpack features and pingbacks. If you use none of those, block it at the server:
# Apache or LiteSpeed (.htaccess)
<Files xmlrpc.php>
Require all denied
</Files>
# Nginx (server block)
location = /xmlrpc.php {
deny all;
}
Blocking at the server is better than a plugin setting. A plugin that disables XML-RPC still lets the request start PHP first. Check the tools you rely on after the change.
Step 6: Add a gate in front of wp-login.php
If only a few people log in, you can put HTTP basic authentication in front of the login page. Bots then hit a server password prompt and never reach WordPress.
# Create the password file outside the web root
htpasswd -c /home/user/.wp-login-htpasswd yourname
# .htaccess in the WordPress root
<Files wp-login.php>
AuthType Basic
AuthName "Restricted"
AuthUserFile /home/user/.wp-login-htpasswd
Require valid-user
</Files>
Protect wp-login.php, not the whole wp-admin folder. Many themes and plugins call wp-admin/admin-ajax.php from the public site, and locking the folder breaks them. If your team works from fixed IP addresses, allowing only those IPs to reach the login page is even stricter.
What about changing the login URL?
Moving the login page to a custom path cuts down log noise from basic bots. It does not protect a weak password, and some plugins break with it. Use it as a convenience on top of the steps above, not instead of them.
Keep the basics in place
Keep WordPress core, themes and plugins updated. Many break-ins use a vulnerable plugin, not the login page.
Use HTTPS on the login page so passwords are never sent in plain text.
Keep regular backups you have tested, so a compromised site can be restored.
Check the access log again a week after your changes to confirm that attempts are being rejected early.
Run WordPress on IM HOST
IM HOST WordPress Hosting includes free SSL, LiteSpeed Cache, NVMe storage and free daily backups for the whole subscription. If you want full control of Nginx rules, firewalls and fail2ban, a Linux VPS gives you full root access on KVM with DDoS protection. Our 24/7 engineers can help you read your logs and decide which of these layers your site needs.
More from our blog
Discover more practical guides and product insights from the IM Host team.
View all articles