Most WordPress security writing is vendor marketing dressed as education. This article is not. The goal is a short, prioritized checklist that tells you what actually reduces your risk of being compromised in 2026, which items are security theatre, and the exact configuration values you need to apply each step. It is written for the site owner who already installed a security plugin, still does not feel safe, and wants to know what to do next.
One number to anchor everything below. According to Patchstack's State of WordPress Security in 2026, 91% of the 11,334 vulnerabilities disclosed in the WordPress ecosystem during 2025 lived in plugins, 9% in themes, and fewer than 1% in WordPress core (six vulnerabilities, all low priority). The median time from public disclosure to mass exploitation was five hours. Hardening is about the 91%, not about core.
In short: what actually matters in 2026
If you only do five things, do these, in order:
- Keep WordPress core, plugins, and themes patched automatically. Five hours is the median window. Manual patching does not fit in it.
- Use a password manager, unique credentials, and a second factor on every administrator account. The overwhelming majority of "hacks" are credential compromises, not exploits.
- Lock down
wp-login.phpandxmlrpc.phpwith rate limiting and a second factor. These two URLs take almost all the brute-force traffic on a typical site. - Set correct file permissions and disable the dashboard file editor. A compromised admin account should not be able to upload a PHP file through the theme editor.
- Take backups you have restored at least once, and store them off the web server. A backup you have never restored is a hope, not a control.
Everything below is an expansion of these five. The later sections are the reference: exact file permission values, wp-config constants, and verification checks you can copy.
Table of contents
- Why "hardening" is not the same as "install a plugin"
- Keep WordPress, plugins, and themes patched automatically
- Use a password manager and enable 2FA on every administrator
- Harden file permissions and ownership
- Lock down wp-login.php and xmlrpc.php
- Disable the dashboard file editor and file modifications
- Limit login attempts and audit failed logins
- HTTPS everywhere, with HSTS
- Backups as a security control
- Security plugins: what they do and what they cannot do
- Reference: the wp-config.php security block
- Reference: file and directory permission values
- Myths worth burning
- When to escalate
Why "hardening" is not the same as "install a plugin"
A WordPress security plugin runs inside WordPress. That is useful, and also a fundamental limit. Every request Wordfence, Sucuri, or iThemes Security evaluates has already passed the firewall, reached your web server, started a PHP worker, loaded WordPress, and handed control to the plugin. At that point the plugin can decide to block the request, but the CPU time and the worker slot are already spent. During a large attack, the worker pool saturates on the plugin itself, and the site goes down defending itself. Cloudways' own analysis of Wordfence's architecture is blunt about this limit.
Hardening is the set of things that happen before or outside a plugin can act. Correct file permissions do not need PHP to run. TLS termination, HSTS, and rate limiting at the web server happen before a PHP worker is picked up. The dashboard file editor constant is checked by WordPress itself, not by any plugin. A two-factor code on every admin login prevents the attack path that dominates real incidents. None of that requires a plugin; all of it matters more than any single plugin you can install.
That is why this article is vendor-neutral. A security plugin can be part of a reasonable setup. It cannot be the setup.
Keep WordPress, plugins, and themes patched automatically
The single highest-value hardening step is also the least glamorous: patch fast and patch automatically. Patchstack's 2026 report pegs the median time from public disclosure to mass exploitation at five hours. Forty-six percent of 2025's vulnerabilities had no patch at the moment of public disclosure. You cannot react to that manually. Automation is the only credible answer.
The good news is that automatic minor updates have been the WordPress default since version 3.7 in October 2013, not a recent 6.x feature. Minor releases (X.Y.Z) carry almost all the security fixes and they install silently by default. What you have to enable yourself is automatic updates for plugins, themes, and major WordPress releases. All three are one-click toggles in wp-admin > Plugins, wp-admin > Appearance > Themes, and wp-admin > Dashboard > Updates. Turn them on for everything you trust, and prune anything you do not trust enough to auto-update.
For developers, the equivalent on the command line is:
# Enable automatic updates for a specific plugin via WP-CLI 2.10+
wp plugin auto-updates enable wordfence
# Enable automatic updates for a specific theme
wp theme auto-updates enable twentytwentyfour
# Enable major-core automatic updates (by default only minor/security are auto)
wp config set WP_AUTO_UPDATE_CORE true --raw
You will know it worked when wp plugin list --format=table shows auto-updates: on in the Auto-updates column for every plugin you enabled.
One piece of editorial guidance. Auto-updates occasionally break things. That risk is real, and the correct answer is not to turn them off; it is to combine them with a backup strategy that can roll a single plugin back in under five minutes. If your backup cannot do that, the backup is the weak link, not the updates. See the backups as a security control section below.
Abandoned plugins are the other half of this problem. A plugin that has not been updated in a year is a plugin that will not receive a patch when its next CVE lands. The WordPress.org plugin directory now shows a warning on any plugin without an update for more than two years and includes install counts, ratings, and the "tested up to" WordPress version. Treat anything that has not been touched in twelve months as a candidate for replacement, and anything without a recent "tested up to" as already compromised by implication.
Use a password manager and enable 2FA on every administrator
Compromised credentials are the attack path the Patchstack report does not need to mention because it is so dominant it is off-chart. Credential-based attacks do not show up in vulnerability counts; they show up in stolen sessions, forwarded password-reset emails, and admins who reused a password on a site that was breached two years ago. The fix is not clever, it is operational.
Three rules, in order of importance:
- Every administrator uses a password manager. No exceptions, not even you. Bitwarden, 1Password, and KeePassXC all work. The password the manager generates is 20 to 30 random characters and you never see it. If a human is typing an admin password, you have already lost the control.
- Every administrator has a second factor. WordPress does not ship with 2FA out of the box, and you need to add it. The Two-Factor plugin maintained by the WordPress core team is one right default for developers; WP 2FA by Melapress is the wizard-driven default for teams with non-technical editors. Both support TOTP (time-based codes) and passkeys (via a companion plugin for Two Factor). The full setup walkthrough, including the grace period that stops you from locking your team out on day one, is in two-factor authentication (2FA) for WordPress.
- Every administrator has a unique email address that they personally control. The password-reset flow is a backup authentication path. Shared
admin@yourcompany.nlmailboxes defeat it.
For sites with more than one admin, enforce all three by policy, not by hope. Audit the user list quarterly and delete ex-employees the day they leave. Dormant admin accounts are compromise waiting for a trigger. For a detailed breakdown of which capabilities each role grants and how to register custom roles, see WordPress user roles and permissions.
One detail on 2FA that confuses people: Application Passwords, introduced in WordPress 5.6, do not replace cookie-based logins or XML-RPC. They supplement them. Application Passwords exist so you can give a mobile app or CI pipeline an API credential without sharing your main password, and so you can still enforce 2FA on interactive logins while leaving programmatic integrations working. They are worth enabling for that use case. They are not a brute-force defense.
Harden file permissions and ownership
File permissions are the quietest hardening you can do, and the one that holds up after a partial compromise. The WordPress hardening guide gives the canonical values: directories at 755, files at 644, wp-config.php at 440 or 400. The reasoning is that the web server user (www-data, nginx, apache) needs to read every file and traverse every directory, but should not need to write to most of them. Only wp-content/uploads/ and (if you use it) wp-content/cache/ need to be writable during normal operation.
Two commands set this correctly on a typical Linux host:
# Run from the WordPress root. Assumes the web server user owns the tree.
# Replace /var/www/html with your install path.
cd /var/www/html
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
# wp-config.php contains database credentials and secret keys
chmod 440 wp-config.php
Ownership is separate from permissions, and just as important. The files should be owned by a user that is not the web server user, with the web server user having read access via the group. A common pattern is chown -R deploy:www-data /var/www/html, so that deploy (your SFTP user) owns the files and www-data (the web server) can read them. That arrangement means a compromised PHP process cannot overwrite theme or plugin files, which is the mechanism most backdoors use to persist. For the full reference on FS_METHOD, ACLs, and SELinux contexts, see WordPress file permissions.
You will know it worked when ls -la wp-config.php shows -r--r----- (mode 440) and stat -c '%a %U %G' . on the WordPress root shows 755 with the expected owner pair.
One caveat. Some shared hosts run PHP as the site owner (suPHP, FPM per-user pools), which breaks the split-ownership model because the PHP process and the file owner are the same account. On those hosts, the hardening benefit is smaller, and the correct compensating control is to disable the dashboard file editor (next section) and keep backups close.
Lock down wp-login.php and xmlrpc.php
wp-login.php and xmlrpc.php are where the traffic lives. Every WordPress site on the public internet sees a steady drizzle of credential-stuffing against wp-login.php, and XML-RPC is the amplification attack: Sucuri documents how the system.multicall method lets an attacker try hundreds of username/password combinations in a single POST request, and how the pingback.ping method can be abused to turn your site into part of a reflected DDoS. Leaving either endpoint wide open is not negligent in 2026; it is an invitation.
Two paths, depending on how you use these endpoints.
If you do not use XML-RPC (most sites)
Disable xmlrpc.php entirely at the web server level. The request should never reach PHP. For nginx:
# In the WordPress server block
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
return 444;
}
For Apache with .htaccess:
# In the WordPress root .htaccess, above the WordPress block
Require all denied
The Jetpack plugin and the classic WordPress mobile app both use XML-RPC, so check whether you actually need it before you disable it. Modern mobile apps and headless setups use the REST API instead, which is authenticated via Application Passwords. Note that the REST API is also an attack surface for user enumeration, so lock it down if you do not need public access to user endpoints.
Verification: run curl -I https://yoursite.nl/xmlrpc.php and confirm you get 403 Forbidden or a closed connection, not 200 OK with a "XML-RPC server accepts POST requests only" body.
For wp-login.php
Protect it with three layers, in this order:
- Rate limiting at the web server or WAF. This stops brute force before it reaches PHP. If you cannot configure your web server directly, a reverse proxy like Cloudflare's WAF at the free tier can rate-limit the path
yoursite.nl/wp-login.phpon five requests per minute per IP. That single rule stops the overwhelming majority of automated attacks dead. For the full nginx/Cloudflare/fail2ban configuration and the Jetpack-preserving method filter, see brute force attack protection in WordPress. - 2FA on every administrator account, as covered above. If a rate-limited login still gets a correct password, the second factor stops the compromise.
- IP allowlisting if your team has static IPs. Add an
.htaccessor nginxallow/denyblock that only permits known IPs to reachwp-login.php. This is the nuclear option and it is excellent when it applies. If your admins work from coffee shops or mobile networks, it does not apply.
Changing the login URL to yoursite.nl/secret-login/ is not in this list. It reduces opportunistic automated traffic, which makes your logs easier to read, but it is not a substitute for rate limiting or 2FA. CVE-2024-2473 and CVE-2024-6289 both exposed hidden login URLs through bugs in the plugins that were supposed to hide them. Obscurity is a log-cleaning tool, not a defense.
Disable the dashboard file editor and file modifications
WordPress ships with a plugin and theme file editor in the admin UI at wp-admin > Appearance > Theme File Editor and wp-admin > Plugins > Plugin File Editor. A compromised administrator account can use it to write a PHP file into wp-content/plugins/ or wp-content/themes/ in thirty seconds. That single file becomes a persistent backdoor.
The editor is enabled by default in every WordPress version. WordPress 4.9 added a sandbox that rolls back fatal errors produced by the editor, but it did not disable the editor itself. If you want it off, you add a constant to wp-config.php:
// Disable the dashboard file editor for themes and plugins
define( 'DISALLOW_FILE_EDIT', true );
For sites where the entire codebase is deployed from a repository and nothing should change on the live server, go one step further and disable all file modifications, which also blocks plugin and theme installs and updates from the dashboard:
// Disable file edits AND plugin/theme install/update from the dashboard
define( 'DISALLOW_FILE_MODS', true );
DISALLOW_FILE_MODS implies DISALLOW_FILE_EDIT; you do not need both. Be aware that turning it on also disables automatic updates from the WordPress side, which means you then need updates handled elsewhere (a CI pipeline, wp-cli, or a managed host). That is fine for sites with disciplined deployment; it is catastrophic for sites without one.
You will know it worked when wp-admin > Appearance no longer shows "Theme File Editor" in the submenu.
Limit login attempts and audit failed logins
Rate limiting at the web server (above) is the first layer. WordPress itself does not track failed logins out of the box, and on most hosts the server rate limit is a blunt instrument. A plugin fills the gap. Limit Login Attempts Reloaded is the minimal option and does one thing well: count failed logins per IP, block the IP after a threshold, and log the attempt. Reasonable defaults are four attempts, a twenty-minute lockout, and a four-hour escalation after three lockouts. Those are also the plugin's defaults.
If you already run a full security suite, it will usually include this feature. Do not stack two plugins that both count failed logins against the same database; pick one and let it own the function.
The audit side matters as much as the block side. A log of failed logins tells you when your site is under attack, which usernames attackers are trying (if they guess admin you are fine; if they guess your real username, someone is targeting you specifically), and whether your blocks are holding. Check it weekly the first month and monthly after.
HTTPS everywhere, with HSTS
Running WordPress without HTTPS in 2026 is not a thing anyone does deliberately; it is a thing that happens when someone configured it once and never checked the details. The checklist is short but the details matter.
- A valid certificate covering every hostname. Let's Encrypt via certbot for self-hosted; whatever the hosting panel provides for managed. The certificate must cover both
yoursite.nlandwww.yoursite.nlif both resolve. A mismatched hostname triggers a browser warning and a drop in search traffic. For a deeper look at certificate types, lifetimes, and the DV/OV/EV distinction, see SSL certificates in WordPress. - Force HTTPS for every URL. The WordPress side is set in
wp-admin > Settings > General: both "WordPress Address" and "Site Address" start withhttps://. The web server side is a 301 redirect fromhttp://tohttps://for every path. A plugin can do this too, but a web server rule is faster and does not depend on PHP running. - Turn on HSTS.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadin your web server response headers tells browsers to never even tryhttp://for your domain for a year. This protects against active downgrade attacks. The MDN HSTS reference is the canonical explanation. - Fix mixed content. If a page is served over HTTPS but includes an
http://image or script, browsers block the resource. Run Why No Padlock against your home page after enabling HTTPS and fix anything it flags. The usual culprit is hard-coded URLs in post content; a search-and-replace in the database cleans it up.
You will know it worked when curl -I https://yoursite.nl/ shows a 200 OK with a Strict-Transport-Security header, and curl -I http://yoursite.nl/ shows a 301 redirect to the HTTPS version.
Backups as a security control
A backup is the recovery plan that makes every other control safer, because it lowers the cost of "restore from last night and investigate at leisure" below the cost of "figure out what an attacker did at 3 AM on a Sunday". That tradeoff is the entire point. The rules:
- Daily automated backups, at minimum. Multi-author sites should be hourly.
- At least one copy stored off the web server. A backup in
wp-content/backups/is not a backup; an attacker with write access towp-content/can delete it. Push backups to S3, Backblaze B2, a different server, or a backup service that pulls from outside. - Retention of at least 30 days. Compromises are often discovered days or weeks after they happen. If your newest backup is already poisoned, you need an older clean one.
- A documented restore procedure that you have executed at least once. The first time you restore from backup should not be during an incident. Do a dry run to a staging environment every quarter and time it. If the restore does not work, the backup does not exist.
- Encrypt the backup at rest if it leaves your infrastructure, especially if it contains personal data from GDPR-scoped users.
Plugins that do this well include UpdraftPlus, BackWPup, and BackupBuddy; most managed hosts run their own system. The specific tool matters less than the four rules above. For the full plan covering retention, off-site storage, and a quarterly restore drill, see WordPress backup strategy.
Security plugins: what they do and what they cannot do
A security plugin is a useful layer, not a complete defense. The honest inventory, for the reference-class plugins (Wordfence, Sucuri, iThemes Security, All In One WP Security):
What they do reasonably well:
- Alert you when a file in
wp-content/changes unexpectedly. - Apply virtual patches for known CVEs faster than you would update the plugin itself.
- Scan uploads and active files for known malware signatures.
- Wrap the login page with 2FA, rate limiting, and CAPTCHA if you don't want to configure those separately.
- Send you an email when a user is locked out or when a file change is detected.
What they cannot do, by architectural limit:
- Block requests before PHP loads. Every request they see has already reached your worker pool. During a large attack, the workers saturate on the plugin itself.
- Protect against server misconfiguration. A world-writable
wp-config.phpis invisible to the plugin. - Stop a compromised admin account. If the attacker has a valid session, the plugin sees legitimate traffic.
- Survive their own scanner load on small hosts. Wordfence's full scan is CPU-heavy; on a $5/month shared host it can knock the site over by itself.
- Replace backups, TLS, or updates. These are separate disciplines.
If you are evaluating one, start with Wordfence (the biggest community), or a hosting-level WAF (Cloudflare, Sucuri's cloud proxy) if you want filtering to happen before requests hit your server. Hosting-level WAFs are strictly better than PHP-level plugins for blocking traffic; PHP-level plugins are strictly better for file monitoring.
Reference: the wp-config.php security block
This is the full wp-config.php snippet to paste, adapted to your site. Insert it above the /* That's all, stop editing! Happy blogging. */ line, below the database and $table_prefix declarations.
// ----- Security hardening block -----
// Disable the dashboard file editor (theme + plugin editor)
define( 'DISALLOW_FILE_EDIT', true );
// Enable automatic updates for major WordPress releases.
// Minor/security updates are auto-installed by default since 3.7.
define( 'WP_AUTO_UPDATE_CORE', true );
// Force all admin requests over HTTPS. Assumes your site is on HTTPS.
define( 'FORCE_SSL_ADMIN', true );
// Raise PHP memory only for administrative tasks (imports, updates).
// Does NOT apply to front-end page loads.
define( 'WP_MEMORY_LIMIT', '128M' );
define( 'WP_MAX_MEMORY_LIMIT', '256M' );
// Debug: log errors to a file, never display on screen in production.
define( 'WP_DEBUG', false ); // set to true only when diagnosing
define( 'WP_DEBUG_LOG', true ); // writes to wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
// Keep only 5 post revisions per post (reduces DB bloat and attack surface).
define( 'WP_POST_REVISIONS', 5 );
// Empty trash every 30 days.
define( 'EMPTY_TRASH_DAYS', 30 );
// ----- End security hardening block -----
Two optional lines you might add for deployment-from-git setups:
// Disable ALL file modifications from the dashboard:
// theme/plugin editor, plugin install, plugin update, theme install.
// Requires updates to be handled outside WP (CI, wp-cli, managed host).
define( 'DISALLOW_FILE_MODS', true );
// Refuse unfiltered HTML from any role, including administrator.
// Only enable if no legitimate author pastes raw HTML into posts.
define( 'DISALLOW_UNFILTERED_HTML', true );
Do not paste these last two without understanding what you are turning off. DISALLOW_FILE_MODS disables the entire dashboard update flow. DISALLOW_UNFILTERED_HTML can break content workflows for sites that paste in third-party embed code.
Reference: file and directory permission values
The full table, matching the WordPress core hardening guide:
| Path | Permission | Octal | Rationale |
|---|---|---|---|
/ (WordPress root) |
drwxr-xr-x |
755 |
Directory traversal by web server, no group or other write |
| All directories under the root | drwxr-xr-x |
755 |
Same as root |
| All files under the root | -rw-r--r-- |
644 |
Readable by web server, writable only by owner |
wp-config.php |
-r--r----- |
440 |
Contains DB credentials and secret keys; readable by group (web server) only |
wp-content/uploads/ |
drwxr-xr-x |
755 |
Must be writable by the owner that runs PHP (not other users) |
wp-content/uploads/* |
-rw-r--r-- |
644 |
Uploaded files are not executable |
.htaccess (Apache only) |
-rw-r--r-- |
644 |
Apache reads it on every request |
Ownership. The tree should be owned by a non-web-server user (deploy, wordpress, the SFTP account), with the web server user (www-data, nginx, apache) in the group. That means PHP reads via group permission, and cannot write to files outside the uploads directory, which is exactly where you want a backdoor to fail.
One exception worth noting. Some plugins genuinely need write access to wp-content/ during install or update (caching plugins generating config files, translation plugins downloading language packs). If you block that, you block the plugin. The cleanest answer is to grant temporary write access during the update, then revert. DISALLOW_FILE_MODS plus an external deployment pipeline is the structural answer for sites where this matters a lot.
Myths worth burning
A short list of things people do that do not help, and the reasons.
"I changed the login URL to /my-secret-login/, so I'm safe." You are not safe; you have made bot logs quieter. Modern vulnerability scanners include login-path discovery, and two CVEs in 2024 (CVE-2024-2473 among them) let attackers leak the hidden URL via bugs in the hiding plugin itself. Rate limiting and 2FA do the work.
"I installed a security plugin, so I don't need to update." The security plugin is a plugin. It has vulnerabilities too. Wordfence's own advisory history is public; Sucuri's and iThemes' are similar. Updates are the control.
"WordPress is insecure." WordPress core had six vulnerabilities in 2025, all low priority. 91% of ecosystem vulnerabilities lived in plugins. The framing is wrong; the plugins you chose are the story.
"My site is too small to attack." Automated attacks do not target; they scan. A site with ten visitors a day sees the same brute-force drizzle as a site with ten thousand. You are a number in a list.
"Hiding wp-admin behind HTTP Basic Auth is enough." It's a useful layer, especially against drive-by scanners and brute force, and the WordPress hardening guide recommends it. But Basic Auth credentials are reusable and sent on every request; the real defense is still a strong password plus 2FA behind it.
When to escalate
Call a specialist when any of these is true, and collect the list below before you do. Every minute they save on context is a minute closer to your site being back.
- A security scan (from a search engine, your hosting provider, or Sucuri/VirusTotal) reports your site as compromised.
- You see unexpected admin users, unexpected posts, or unexpected redirects to unrelated domains. If that matches your situation, the WordPress hacked / malware redirect cleanup article walks through the full incident response.
- Your host sends a warning about malware or phishing served from your site.
- A backup restore does not produce a clean site, and the next older backup is also dirty.
- You attempted hardening, and the site broke in a way you cannot explain.
Collect before you ask:
- The WordPress version, PHP version, and active theme.
- A list of active plugins with versions (from
wp plugin list --format=csvorwp-admin > Plugins). - The most recent entries in
wp-content/debug.logif it exists. - The most recent entries in your web server's access and error logs.
- A list of admin users and the date each was created.
- Whether you have a known-good backup and how old it is.
- A timeline of what changed in the 72 hours before the symptoms appeared.
If the symptoms are a login you cannot reach rather than a visible compromise, the full diagnostic flow lives in cannot log in to WordPress, and for the specific case of a login that succeeds and then bounces you back, see WordPress login redirect loop. If your hardening work broke wp-config.php and the site is now blank, the recovery path is in white screen of death in WordPress.