WordPress hardening best practices
When to use this skill
Use this skill for site/environment configuration, as opposed to plugin code:
- Editing
wp-config.php(keys, constants, debug). - Writing/reviewing
.htaccess(Apache) or server-block (nginx) rules. - Setting filesystem permissions and ownership.
- Locking down
wp-admin, XML-RPC, REST user enumeration, and the uploads dir. - Pre-launch hardening checklists and deployment review.
This complements the secure-coding skills: even perfect code runs on a host that must be configured to fail safely.
Core principles (and why they matter)
- Disable in-dashboard file editing in production.
DISALLOW_FILE_EDITremoves the plugin/theme editor — a single compromised admin session otherwise becomes code execution. - Force TLS for admin and logins.
FORCE_SSL_ADMINstops credentials and cookies traveling in cleartext. - Never display errors in production. Stack traces leak paths, queries, and secrets.
WP_DEBUGoff,display_errorsoff; log to a non-web-readable file if needed. - Block PHP execution where only data should live. An attacker who slips a PHP file
into
wp-content/uploadsgets RCE unless the server refuses to execute it there. - Protect sensitive files.
wp-config.php,.htaccess,readme.html,debug.log, and dotfiles should not be web-readable. - Least privilege on the filesystem. Files
644, directories755,wp-config.php640/600; web server should not own files it doesn't need to write. - Unique, strong security keys/salts. They invalidate stolen cookies; regenerate if leaked.
- Reduce attack surface. Disable XML-RPC if unused, block user enumeration, keep core/ plugins/themes updated, remove unused plugins.
Step-by-step implementation
- In
wp-config.php: set unique salts,DISALLOW_FILE_EDIT,FORCE_SSL_ADMIN, disable debug display; optionallyWP_AUTO_UPDATE_CORE,DISALLOW_FILE_MODSfor locked builds. - Place
wp-config.phppermissions at640/600; ensure it is above or protected from the web root. - Add server rules to deny PHP in
uploads, protect sensitive files, and (optionally) restrictwp-admin/xmlrpc.php. - Set file/dir permissions to least privilege.
- Keep everything updated; remove what you don't use.
- Add abuse controls for public surfaces: comment moderation on, pingbacks off if
unused, and — for bot-heavy sites — CAPTCHA/Turnstile on login and registration via
an established plugin (hand-rolled CAPTCHAs fail in both directions). Pair with the
login throttle from
authentication-session-security; XML-RPC stays off unless needed.
Supporting references
| Reference | Load when |
|---|---|
| WordPress hardening checklist | Before final verification of the wordpress hardening controls. |
| Pre-launch (go-live) checklist | Reviewing a production launch across configuration, code, data, and operations. |
| Apache and nginx hardening rules | Implementing server hardening for sensitive files, uploads, and restricted endpoints. |
| Hardened wp-config.php constants | Implementing the wp-config.php hardening flow for salts, HTTPS, debugging, editing, and updates. |
Common AI mistakes / anti-patterns
Mistake 1 — Leaving debug output on in production
// ❌ Insecure: errors rendered to visitors leak paths, SQL, secrets.
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_DISPLAY', true );
// ✅ Secure: log privately, never display, in production.
define( 'WP_DEBUG', false );
// If you must debug on a live box, at least never display:
@ini_set( 'display_errors', '0' );
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_DEBUG_LOG', true ); // writes to wp-content/debug.log — block it from the web
Mistake 2 — Allowing the dashboard file editor
// ❌ Risky: compromised admin = arbitrary PHP via Appearance/Plugins editor.
// (default: editor enabled)
// ✅ Secure: disable file editing (and optionally all file mods).
define( 'DISALLOW_FILE_EDIT', true );
// Fully locked deploy (no plugin/theme install/update via UI):
define( 'DISALLOW_FILE_MODS', true );
Mistake 3 — 777 permissions "to make it work"
# ❌ Insecure: world-writable lets any local process modify your site.
chmod -R 777 wp-content
# ✅ Secure: least privilege.
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod 640 wp-config.php
Mistake 4 — Uploads directory that executes PHP
# ❌ Insecure: a smuggled shell.php in uploads runs.
# (no restriction)
# ✅ Secure: deny PHP execution in uploads (Apache). Place in wp-content/uploads/.htaccess
<FilesMatch "\.(?i:php|php\d|phtml|phar)$">
Require all denied
</FilesMatch>
Mistake 5 — Default/duplicated security keys
// ❌ Insecure: placeholder salts (or copied between sites).
define( 'AUTH_KEY', 'put your unique phrase here' );
// ✅ Secure: generate unique values from the official salt API and rotate if leaked.
// https://api.wordpress.org/secret-key/1.1/salt/
define( 'AUTH_KEY', '...64 random chars...' );
// (all eight: AUTH_KEY/SALT, SECURE_AUTH_KEY/SALT, LOGGED_IN_KEY/SALT, NONCE_KEY/SALT)
Mistake 6 — Leaving XML-RPC enabled when unused
// ❌ Risky: XML-RPC is a brute-force and pingback amplification vector if not needed.
// (default: enabled)
// ✅ Secure: disable XML-RPC entirely if the site does not need it.
add_filter( 'xmlrpc_enabled', '__return_false' );
// Or block at the server level (see htaccess-hardening.conf).
Mistake 7 — Allowing REST user enumeration
# ❌ Risky: /wp-json/wp/v2/users/ reveals usernames to unauthenticated visitors.
curl https://example.com/wp-json/wp/v2/users/
// ✅ Safer: require authentication for the users endpoint.
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( isset( $endpoints['/wp/v2/users'] ) ) {
$endpoints['/wp/v2/users'][0]['permission_callback'] = static function () {
return is_user_logged_in();
};
}
return $endpoints;
} );
Mistake 8 — Application Passwords enabled without review
Application Passwords (WordPress 5.6+) are powerful for integrations but create a long-lived credential surface. Disable them if unused, or restrict them to users who actually need machine access.
// ✅ Secure: disable Application Passwords if the site does not use them.
add_filter( 'wp_is_application_passwords_available', '__return_false' );
Related: see the secrets-credentials-management skill for storing and handling API
keys and tokens safely. See the security-headers-csp skill for application-level
security headers (HSTS belongs in this skill's server config; the rest is code-level), and
the dependency-supply-chain-security skill for keeping bundled libraries and CDN assets
from rotting.
Correct code examples
Hardened wp-config.php constants are in
references/wp-config-hardening.php; Apache and
nginx rules (deny PHP in uploads, protect wp-config.php/.htaccess/debug.log, limit
xmlrpc.php) are in references/htaccess-hardening.conf;
the consolidated pre-launch sweep is references/go-live-checklist.md.
Checklist
- Unique, strong security keys/salts set (all eight).
-
DISALLOW_FILE_EDITenabled in production. -
FORCE_SSL_ADMINenabled; site served over HTTPS. -
WP_DEBUG/WP_DEBUG_DISPLAYoff in production; logs not web-readable. - PHP execution denied in
wp-content/uploads. -
wp-config.php,.htaccess,debug.log, dotfiles not web-accessible. - Permissions least-privilege: dirs
755, files644,wp-config.php640/600. - XML-RPC disabled/limited if unused; user enumeration limited.
- Core, plugins, themes kept updated; unused extensions removed.
- Admin accounts use strong passwords + 2FA where possible.