How to Harden WordPress
7 min read
A default WordPress install comes with a lot of problems out of the box. The login URL sits at /wp-login.php where every bot knows to look. Password attempts are unlimited. The admin includes a file editor that hands direct code execution to anyone who gets in. And your WordPress version is advertised in page metadata to whoever asks.
None of it is difficult to fix. You just have to know what to change and bother to change it. This post covers the steps in the order I'd do them on a new site.
Start with wp-config.php
wp-config.php is where your database credentials, security keys, and a handful of WordPress behaviour settings live. Everything you tighten here protects things further up the stack, which is why it's the first place to look.
Disable the file editor
The WordPress admin includes a built in theme and plugin editor. It is a convenience feature that has no place on a production site. If an attacker gains access to an admin account, the file editor hands them direct code execution on your server.
Add this to wp-config.php:
define( 'DISALLOW_FILE_EDIT', true );
Regenerate your security keys and salts
WordPress uses keys and salts to encrypt session tokens and cookies. If you've never changed them from the defaults, generate a fresh set from the official API:
https://api.wordpress.org/secret-key/1.1/salt/
Replace the existing keys in wp-config.php with the new ones. If your site has ever been compromised, doing this immediately invalidates all active sessions.
Lock down debug settings
Debug mode should never be on in production. Make sure these are set correctly:
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );
Force SSL for the admin area
If your site runs HTTPS, which it should, force the admin over SSL:
define( 'FORCE_SSL_ADMIN', true );
Tighten file permissions
wp-config.php should be readable only by the user running PHP. Set permissions to 440:
chmod 440 /var/www/wp-config.php
Secure the admin area
Move the login URL
The default login URL is /wp-login.php. Every bot on the internet knows that, which is why automated brute force attempts are a constant background noise on WordPress sites. Change the login path to something unpredictable and most of that noise hits a 404 instead of your login form.
WPS Hide Login handles this cleanly without modifying core files. Pick a path that isn't guessable and keep a record of it somewhere safe.
Limit login attempts
WordPress allows unlimited password attempts by default. That isn't acceptable on any production site. Wordfence handles limiting as part of its free tier. Limit Login Attempts Reloaded is a good alternative if you don't want Wordfence for some reason.
A sensible configuration: lock an IP out for 20 minutes after 5 failed attempts, with a longer lockout for repeat offences.
Enforce two factor authentication
Two factor authentication does more for a WordPress site's security than almost any other single change. Even a leaked or guessed password isn't enough to get an attacker in on its own.
Wordfence includes 2FA in its free tier. Turn it on and enforce it for all administrator and editor accounts. Don't rely on voluntary opt in, most users will never bother.
Change weak usernames
If your admin account is called "admin", change it. It is the first username every brute force script tries, by a wide margin. WordPress doesn't let you rename usernames from the dashboard, but you can edit one directly in the database, or create a new admin account and delete the old one.
Protect core files and directories
Block access to sensitive files
Several files in a WordPress installation should never be publicly accessible. Add these rules to your Nginx configuration:
location ~* /(wp-config\.php|xmlrpc\.php|readme\.html|license\.txt) {
deny all;
}
location ~* /\. {
deny all;
}
The readme.html and license.txt files are worth blocking because they expose your WordPress version number to anyone who asks.
Block PHP execution in the uploads directory
The uploads directory needs to be writable by WordPress, but it should never execute PHP files. If an attacker manages to upload a malicious PHP file and your server executes it, you have a serious problem.
In Nginx:
location ~* /wp-content/uploads/.*\.php$ {
deny all;
}
This single rule blocks a huge class of attacks. Webshell uploads via vulnerable plugins are one of the more common ways WordPress sites get compromised, and a PHP block in the uploads directory stops the uploaded file being executable even if the upload itself slips through.
Disable directory listing
Without an index file in a directory, a web server will list its contents to anyone who visits, unless you tell it not to. In Nginx this is off by default. In Apache, add Options -Indexes to your configuration.
Disable XML-RPC
XML-RPC is a legacy protocol that has been consistently exploited over the years. It supports a method that lets attackers bundle hundreds of login attempts into a single HTTP request, bypassing login attempt limits that only count individual requests.
Unless you have a specific tool that requires it, disable XML-RPC at the server level:
location = /xmlrpc.php {
deny all;
}
The WordPress REST API has replaced XML-RPC for almost every legitimate use case.
Sort out file permissions
Incorrect file permissions are a common misconfiguration on WordPress sites. The rules are simple.
- Directories:
755 - Files:
644 wp-config.php:440- Never:
777
To reset permissions in bulk on a self managed server:
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 440 /var/www/wp-config.php
Check for any PHP files that are world writable, because those are a serious risk:
find /var/www/html -name "*.php" -perm -o+w
If that command returns anything, fix it immediately.
Replace WP-Cron
WordPress ships with a pseudo cron system that runs on every page visit. On busy sites that's a lot of unnecessary work. On quiet sites, scheduled tasks might not fire for hours because nobody is visiting. And because wp-cron.php is publicly accessible, it can be abused in denial of service attacks.
Disable it in wp-config.php:
define( 'DISABLE_WP_CRON', true );
Then add a real server side cron job:
*/15 * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/html/ --quiet
Block user enumeration
WordPress exposes usernames via predictable URL patterns by default. Visiting /?author=1 redirects to /author/username/, handing over a valid username to anyone who tries it. The REST API exposes a full user list at /wp-json/wp/v2/users.
Block author archive redirects by adding this to your theme's functions.php or a code snippet plugin:
add_action( 'template_redirect', function() {
if ( is_author() && ! is_user_logged_in() ) {
wp_redirect( home_url(), 301 );
exit;
}
});
And remove the users endpoint from the REST API:
add_filter( 'rest_endpoints', function( $endpoints ) {
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
return $endpoints;
});
Keep everything updated
Outdated plugins and themes are the single biggest reason WordPress sites get compromised. Once a vulnerability is disclosed and patched, the details often become public information. Sites still running the old version become easy targets within days.
Apply security updates promptly. Remove plugins and themes you aren't actively using. Each one is another thing that can have a vulnerability discovered in it next week.
Add a firewall
A Web Application Firewall sits between incoming traffic and your site, and drops the obvious bad stuff before it ever reaches WordPress. On a WordPress site I'll usually run two of them, because they do different jobs.
Wordfence runs inside WordPress itself and understands what a WordPress request is supposed to look like. The free tier covers firewall, malware scanning, login security, and 2FA.
Cloudflare sits out at the edge, in front of your server, and handles volumetric attacks, DDoS, and filtering of known bad IPs before traffic reaches your origin. The free tier is enough for most small sites.
Together they cover the edge and the application. That pairing is the default I'd use on any WordPress site.
After hardening
Working through this guide closes off the most common attack vectors on a WordPress site. It won't keep the site secure forever. Plugins get new vulnerabilities every week. PHP versions hit end of life. Passwords leak in third party breaches that have nothing to do with your site.
The sites that stay secure long term are the ones where someone keeps looking at them. Set up and forget doesn't work.
Protect My WP goes further than this post, through ongoing maintenance, backups, monitoring, and what to do when something does go wrong. If you want the full picture, that's where to go next.
Get the book for £19.
Get the free WordPress Security Checklist
The security checks I'd run through on any WordPress site, delivered straight to your inbox.
Want to go deeper?
The first chapter of Protect My WP is free. Start with the foreword, then read Chapter 1 on hosting and server security.