wordpress
Hardening WordPress in 2026 — the checklist we actually run on customer sites
Most WordPress security guides are 80% noise. Here are the controls that actually stop the attacks we see every week.
May 19, 2026 · 9 min · by Sudhanshu K.
Hardening WordPress in 2026 — the checklist we actually run on customer sites
Most "Top 50 WordPress Security Tips" articles are noise. They list things like "use a strong password" and "keep plugins updated" alongside genuinely useful controls, and the reader has no way to tell which items matter and which are filler.
So here's a different kind of post. This is the checklist our team runs against every new WordPress site that comes under our managed hosting. Each item is here because we've seen the attack it prevents land on a real customer in the last 18 months.
If you only run the controls in this list, you'll defend against approximately 95% of what actually shows up at WordPress origins in 2026. Everything else is single-digit-percent gain.
The wp-config.php hardening pass
// /var/www/html/wp-config.php — the controls that matter
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true);
define('AUTOMATIC_UPDATER_DISABLED', false);
define('WP_AUTO_UPDATE_CORE', 'minor');
define('FORCE_SSL_ADMIN', true);
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('CONCATENATE_SCRIPTS', false);The two most important here:
DISALLOW_FILE_EDITremoves the in-dashboard theme/plugin editor. If an attacker compromises an admin account, they can no longer drop arbitrary PHP through the UI. They have to find another way in, which means writing actual exploit code instead of just pasting webshells.DISALLOW_FILE_MODSgoes further and prevents any plugin or theme install/update from the dashboard. This is heavy-handed but correct for production sites where deployments come through CI, not through Bob clicking "Update" at 11pm.
File-system permissions, the way it should be
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 640 /var/www/html/wp-config.php
chown -R root:www-data /var/www/html
chown www-data:www-data /var/www/html/wp-content/uploadsThe principle: PHP-FPM should not own the source code it executes. Read-only is correct. The only directory PHP-FPM should be able to write to is wp-content/uploads, and ideally that's mounted noexec so even if a malicious file lands there, it can't be executed:
/dev/xvdb1 /var/www/html/wp-content/uploads ext4 defaults,noexec,nosuid,nodev 0 0
This single mount option (noexec on the uploads directory) defeats roughly half of the WordPress webshells we see in incident response. They drop a .php file into uploads, but the kernel refuses to execute it. Game over.
Disable XML-RPC unless you genuinely need it
XML-RPC was useful when the Jetpack mobile app and trackbacks were a thing. In 2026, for most sites, it's a brute-force amplifier:
location = /xmlrpc.php {
deny all;
return 403;
}If you do need it (you're using the WordPress mobile app or a specific integration), at least rate-limit it:
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=2r/s;
location = /xmlrpc.php {
limit_req zone=xmlrpc burst=5 nodelay;
fastcgi_pass php_upstream;
}The XML-RPC system.multicall method lets an attacker try dozens of password guesses in a single HTTP request, which is why naive fail2ban rules that count failed logins by HTTP request rather than by login attempt completely miss it. We've watched brute-force runs of 50,000 passwords land in 200 requests.
REST API: lock down user enumeration
add_filter('rest_endpoints', function($endpoints) {
if (isset($endpoints['/wp/v2/users'])) unset($endpoints['/wp/v2/users']);
if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
return $endpoints;
});/wp-json/wp/v2/users returns the list of usernames on the site to anyone who asks. That's half of the credentials needed for a brute-force run — you've just told the attacker exactly which usernames are real. Same goes for /?author=1, which 302-redirects to the user's profile and reveals their slug. Block both.
Fail2ban rules that actually work
Default Fail2ban rules tend to watch auth.log and match SSH brute-force. For WordPress, you want a custom jail watching the Nginx access log for failed wp-login.php POSTs:
# /etc/fail2ban/jail.d/wordpress.conf
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 600
bantime = 86400And the matching filter that counts only POSTs to wp-login.php returning 200 (successful authentication attempt — which from outside looks the same whether the password was right or wrong, but we count any POST and ban on volume):
# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php HTTP/.*" .*$
ignoreregex =For sites under sustained attack, this drops the attack volume by orders of magnitude. For sites we know are being actively targeted, we escalate to Cloudflare's WAF rules instead — same logic, but at the edge instead of the origin. We can usually get this in place within an hour through our server-under-threat emergency response.
2FA on the dashboard, no exceptions
Every admin and editor account gets a TOTP second factor. We use the WP 2FA plugin from Melapress (formerly WPWhiteSecurity), configured to enforce 2FA enrollment on first login. Grace periods are how 2FA never gets enrolled — make it mandatory.
For sites with very small admin teams (a handful of users), we go further and require U2F/WebAuthn hardware keys. The phishing resistance of WebAuthn vs TOTP is night and day in 2026, and YubiKeys are $50.
File integrity monitoring (FIM)
The single best post-compromise detection. We use AIDE or Tripwire to baseline /var/www/html after a deploy, then check daily for any file that's changed outside the deploy window. The output goes to a SIEM and any deviation pages the on-call:
aide --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Daily cron
aide --check | mail -s "AIDE diff $(hostname)" security@example.comIf a webshell lands in /wp-content/uploads/.well-known/cache.php, AIDE catches it within 24 hours regardless of what other defences failed. This is the kind of control that turns "we got pwned for 6 months and didn't know" into "we got pwned for 6 hours."
Plugin and theme provenance
The most prosaic and most ignored control. We maintain an allowlist of plugins and themes per site. Anything not on the list gets removed. The allowlist is reviewed quarterly — anything that hasn't seen an update in 12 months gets a flag and a conversation with the customer about whether to replace it.
In 2026, the biggest source of WordPress compromise is not WordPress itself — it's a long-tail abandoned plugin with an authenticated SQL injection that nobody's looking at. Sometimes the fix is a one-click update; sometimes it's "this plugin is abandoned, we need to migrate off it before someone else finds the bug." Either way, you need to know it's there.
What we don't recommend
To close the loop, the things you'll see in other guides that we don't consider worth the operational cost:
- Renaming wp-login.php — security by obscurity, breaks integrations, every scanner finds it anyway in 30 seconds
- Hiding the WordPress version number — same thing, doesn't stop targeted attacks
- CAPTCHA on every form — terrible accessibility, modern rate-limiting at the edge is better
- Disabling the REST API entirely — breaks Gutenberg and most modern plugins; selectively restricting endpoints is correct
If your WordPress site is currently exposed and you don't have most of the above in place, that's a pen-testing engagement waiting to happen. Honestly, even sites that do have most of these still benefit from a fresh set of eyes — security drifts quietly. We're happy to do the audit.
Sudhanshu K. leads cybersecurity engagements at EdgeServers, a unit of RemotIQ Pty Ltd (ABN 91 682 628 128). She has spent 14 years finding and fixing WordPress compromises across hundreds of customer sites.