How to Make Life Miserable for Bots and Hackers Attacking Your WordPress Site
Every WordPress site gets attacked. Yours too. The day you put a fresh install online, automated scanners will find it, usually within hours. Check your access logs after a week and you see hundreds of requests to /wp-login.php, /xmlrpc.php, /wp-config.php.bak, /.env, and a long list of plugin paths that are not even installed on your site.

Step Zero: The Boring Stuff That Actually Matters
Before the fun tricks, the fundamentals. Skip these and nothing else in this post will save you.
Keep everything updated. Core, plugins, themes. Most successful WordPress hacks I've cleaned up exploited a known vulnerability that a patch had already fixed. The site owner just never clicked "update." Enable automatic updates for minor core releases and for plugins you trust, and test bigger updates on staging.
Delete what you don't use. A deactivated plugin is still code on your server, and its vulnerable files can still be reached directly. The same goes for those three default themes you never activated. If it's not in use, delete it.
Never use nulled themes or plugins. A "free" premium theme from a random forum often comes with a backdoor. The $60 you saved will cost you far more later.
Have off-site backups and test restoring them. A backup you've never restored is just a hope. Store backups somewhere other than the server they protect, because when a server gets compromised, the backups sitting on it often go too.
With that covered, let's make attackers' lives harder.
1. Lock Down the Login Page
wp-login.php is the most attacked URL on any WordPress site. Bots try credential combinations there all day long. Here are several layers, from simplest to strongest.
Use strong passwords and 2FA
This is the baseline. Use a password manager, give every admin account a long random password, and enable two-factor authentication. Wordfence, Two Factor, and similar plugins handle it well. Even if someone gets a password, 2FA stops them.
Also, don't use admin as a username. It's the first thing every bot tries.
Limit login attempts
By default, WordPress lets anyone try passwords forever. A login-limiting plugin (Limit Login Attempts Reloaded, or the built-in protection in Wordfence) locks out an IP after a few failures. A bot that hoped to try 10,000 passwords now gets five.
Stop telling attackers which part was wrong
WordPress helpfully says whether the username or the password was incorrect. That tells a bot which usernames are real. Make the error message generic:
add_filter('login_errors', function () {
return 'Invalid credentials.';
});
Put HTTP Basic Auth in front of the login page
This is my favorite trick, and one of the most effective. Add a second, server-level password prompt in front of wp-login.php. Bots hitting the login page get a 401 from the web server before WordPress or PHP even runs. Their attack fails almost immediately, and your server barely notices.
On Apache, create a .htpasswd file and add this to your .htaccess:
<Files wp-login.php>
AuthType Basic
AuthName "Restricted"
AuthUserFile /home/youruser/.htpasswd
Require valid-user
</Files>
On Nginx:
location = /wp-login.php {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
One warning: don't put Basic Auth on the entire /wp-admin/ directory without exceptions. Many plugins and themes use /wp-admin/admin-ajax.php on the frontend, and blocking it will break things for regular visitors.
Change the login URL
Plugins like WPS Hide Login move your login page from /wp-login.php to something like /my-secret-door. On its own this is security through obscurity, not real protection. Still, it removes nearly all of the automated noise, because bots don't know where to knock. Use it as an extra layer, not as your only defense.
Restrict login by IP (if you can)
If you and your team always log in from fixed IPs or a VPN, allow only those addresses to reach the login page. For everyone else, it simply doesn't exist. This is the strongest option on the list, but it only works if your IPs don't change often.
2. Close the Back Doors Bots Love
Disable XML-RPC
xmlrpc.php is an old remote-publishing interface that most sites no longer need. Attackers love it because older WordPress setups let them test many passwords in a single request, and it's also used for pingback abuse.
If you don't use Jetpack or apps that rely on XML-RPC, block it at the server level.
Apache:
<Files xmlrpc.php>
Require all denied
</Files>
Nginx:
location = /xmlrpc.php {
deny all;
}
Blocking it at the server is better than a plugin that "disables" it, because the request never reaches PHP.
Stop user enumeration
Visit yoursite.com/?author=1 on a default install and WordPress will redirect you to /author/username/. Congratulations, you've just given a bot your admin's username. The REST API does the same thing at /wp-json/wp/v2/users.
Close both with a small must-use plugin (wp-content/mu-plugins/security.php):
<?php
// Block ?author=N enumeration
add_action('init', function () {
if (!is_admin() && isset($_GET['author'])) {
wp_safe_redirect(home_url(), 301);
exit;
}
});
// Hide user endpoints from anonymous REST API requests
add_filter('rest_endpoints', function ($endpoints) {
if (!is_user_logged_in()) {
unset($endpoints['/wp/v2/users']);
unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
}
return $endpoints;
});
I put these in mu-plugins deliberately. Must-use plugins load automatically and can't be deactivated from the admin panel, so someone who gets into wp-admin can't simply switch them off.
Disable application passwords if you don't use them
Application passwords let external tools authenticate to the REST API. They're useful for integrations. If you don't have any, turn them off:
add_filter('wp_is_application_passwords_available', '__return_false');
3. Harden wp-config.php
A few lines in wp-config.php close off whole categories of attacks.
// Disable the built-in theme and plugin file editor
define('DISALLOW_FILE_EDIT', true);
// Force HTTPS in the admin area
define('FORCE_SSL_ADMIN', true);
DISALLOW_FILE_EDIT matters a lot. Without it, anyone who gets an admin login can edit your theme's PHP files directly from the browser and drop in a backdoor in seconds. With it, a stolen admin login is still bad, but much less of a disaster.
There's also DISALLOW_FILE_MODS, which blocks installing and updating plugins from the admin entirely. It's even stronger, but it also disables updates from wp-admin, so only use it if you deploy updates another way (Git, WP-CLI, CI).
Make sure wp-config.php itself can't be fetched over HTTP, even if PHP breaks:
<Files wp-config.php>
Require all denied
</Files>
And while you're there, generate fresh security keys from the official WordPress salt generator. If you ever suspect a compromise, rotating these logs out every active session.
4. Don't Let Uploaded Files Execute
A classic attack goes like this: find a vulnerable upload form, upload image.php disguised as a picture, then request it directly to run code on your server.
The fix is simple. Nothing in your uploads folder should ever be executable PHP. Block it.
Apache, in wp-content/uploads/.htaccess:
<FilesMatch "\.(php|phtml|php[0-9])$">
Require all denied
</FilesMatch>
Nginx:
location ~* ^/wp-content/uploads/.*\.(php|phtml|php[0-9])$ {
deny all;
}
Even if an attacker manages to upload a malicious file, they can't run it. You've turned a full server takeover into a harmless file sitting in a folder.
While you're in the server config, turn off directory listing so nobody can browse your folders:
Options -Indexes
5. Block Junk Before It Reaches WordPress
Every request that reaches PHP costs your server CPU and memory. The cheapest way to handle a bot is to drop it before WordPress ever loads.
Put a WAF in front of your site
Cloudflare's free plan alone filters out a surprising amount of garbage. Turn on bot protection and add a few custom rules. Some I commonly use:
- Challenge or block all requests to
/wp-login.phpand/xmlrpc.phpfrom countries where you have no editors (if your whole team is in Poland, nobody from elsewhere needs your login page) - Rate-limit
/wp-login.php, for example a handful of POST requests per minute per IP - Block requests for paths that should never be fetched:
/.env,/.git/,wp-config.php.bak,/phpmyadmin
A good WAF does the heavy lifting at the network edge, so your server never sees most of the attack traffic.
Use fail2ban on your own server
If you run your own VPS, fail2ban is excellent. The WP fail2ban plugin writes failed logins to the system auth log, and fail2ban bans offending IPs at the firewall level. After a few failed attempts, the attacker's IP can no longer even connect to your server. Not to WordPress, not to anything.
This is where you truly make life miserable for bots. Their requests stop getting responses, connections time out, and their scanning slows to a crawl.
6. Protect Forms with Honeypots
Contact forms, comment forms, and registration forms attract spam bots constantly. Before reaching for an annoying CAPTCHA, try a honeypot.
The idea is simple. Add a form field that is hidden from humans with CSS. Real users never see it, so they leave it empty. Bots fill in every field they find. If that hidden field contains anything, it's a bot, and you quietly discard the submission.
Many form plugins (Contact Form 7 add-ons, Gravity Forms, WPForms) have honeypot options built in. Turn them on.
If you need more, Cloudflare Turnstile is a solid choice. It's free, privacy-friendly, and usually invisible to real users, unlike the old "click all the traffic lights" puzzles that frustrate humans more than bots.
7. Set Traps
Here's a fun one. Bots behave in predictable ways, and you can use that against them.
The forbidden-path trap. Add a path to robots.txt as disallowed, for example /secret-admin-backup/. Legitimate crawlers respect robots.txt and stay away. Badly behaved bots read robots.txt specifically to find "interesting" paths and visit them. Configure fail2ban or a Cloudflare rule so that anyone requesting that path gets banned. Only bad actors fall in.
The fake-file trap. Requests for /.env, /wp-config.php.bak, or /.git/config on a WordPress site come from scanners hunting for leaked credentials. No real visitor will ever ask for them. Treat any such request as an automatic ban.
These traps are cheap, and they're satisfying. Bots that came looking for an easy win reveal themselves in the very first request.
One important line: traps should be defensive. Block, ban, waste their time. Don't try to "hack back," send malicious payloads, or retaliate. That can get you into legal trouble and often hits innocent machines that were compromised and used as proxies.
8. Add Security Headers
Security headers tell browsers how to behave and protect your visitors from some attacks like clickjacking and content sniffing. A reasonable baseline:
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
Only add Strict-Transport-Security once your site works fully on HTTPS, including every subdomain. Browsers will remember it, and if HTTPS breaks later, visitors won't be able to reach you.
A Content Security Policy is the most powerful header, but on WordPress with many plugins it's easy to break things. Start in report-only mode and tighten it gradually.
9. Watch What's Happening
You can't defend what you can't see. Set up at least basic monitoring:
- File integrity checks. Wordfence and similar tools compare your core files against the official versions and alert you when something changes. Unexpected changes to core files almost always mean trouble.
- Login notifications. Get an email when an admin logs in. If you get one and it wasn't you, you know immediately.
- Uptime monitoring. A free uptime monitor tells you when your site goes down, which is sometimes the first sign of an attack or a broken update.
- Read your logs occasionally. Spend ten minutes looking at your access logs. You'll learn a lot about what bots are trying, and you'll spot new patterns to block.
What I Don't Bother With
A few popular "security" tips aren't worth much in my experience:
Changing the database table prefix from wp_ to something random. It only helps against a narrow set of poorly written SQL injection attempts. If someone has SQL injection on your site, the prefix is the least of your problems. On a new install, sure, change it. On an existing site, the risk of breaking things outweighs the benefit.
Hiding the WordPress version. Removing the version from the page source is fine, but bots don't check your version before attacking. They just try every known exploit. Keeping WordPress updated matters far more than hiding which version you run.
Installing five security plugins at once. They conflict, slow down your site, and duplicate each other's work. Pick one solid security plugin and do the rest at the server level.
The Quick Checklist
If you only have half an hour, do this:
- Update everything and delete unused plugins and themes
- Strong passwords, 2FA, and no
adminusername - Limit login attempts and use generic login errors
- Block
xmlrpc.phpat the server level - Block user enumeration
- Add
DISALLOW_FILE_EDITtowp-config.php - Block PHP execution in the uploads folder
- Put Cloudflare (or another WAF) in front of the site
- Set up off-site backups and test a restore
Then, when you have more time, add Basic Auth on the login page, fail2ban, honeypots, traps, and security headers.
The Bottom Line
Most WordPress hacks aren't sophisticated. They're automated attacks looking for unpatched plugins, weak passwords, and default configurations. The attacker is a script that never gets tired, but it is also lazy and predictable.
You don't need to outsmart a determined expert. You need to be a worse target than millions of other sites. Every layer you add, whether it's a login prompt before the login page, a blocked XML-RPC endpoint, or a trap that bans scanners on sight, makes attacking you slower, more expensive, and less rewarding.
And honestly? There's real satisfaction in opening your logs and watching thousands of bot requests hit a wall and go nowhere.
Make them work for it. Most of them won't bother.
