Security
Hardening wp-config.php: the checklist we run on every site
wp-config.php is the one file every WordPress site has and almost nobody audits after install. This is the checklist we apply on every site we provision, with each setting explained so you can decide whether it fits your site rather than copy-pasting and hoping.
Contents
- · Why wp-config is the right file to harden
- · The checklist, with explanations
- · Salts and rotation
- · Restricting wp-login.php via .htaccess
- · Auto-updates: our actual policy
- · The validation script we run after
wp-config.php is the most load-bearing file in a WordPress install. It defines the database connection, the table prefix, the authentication keys, the debug mode, and dozens of behavioural constants. It is also the file most WordPress tutorials tell you to edit once at install time and never touch again. We have found that to be backwards: a well-tuned wp-config is worth more than most security plugins, and it costs nothing.
What follows is the exact checklist we apply when we provision a WordPress site. It is not exhaustive. It is the set of changes with the highest benefit-to-risk ratio, ordered roughly by how important each one is.
Why wp-config is the right file to harden
A security plugin runs inside WordPress. If an attacker can compromise WordPress itself (through a plugin vulnerability, a weak admin password, a theme exploit), a security plugin is one of the first things they can disable. wp-config.php is loaded before plugins, before themes, before most of WordPress's own code. Constants defined here cannot be overridden at runtime. A site whose wp-config is hardened has a smaller attack surface even against authenticated attackers.
The checklist, with explanations
<?php
// wp-config.php — hardening block, paste above the "That's all, stop editing!" line
// --- 1. Disable the file editor in wp-admin ---
// Without this, an admin account (or anyone who compromises one) can edit
// plugin and theme PHP files directly from the dashboard. That is a fast
// path to a full site compromise.
define( 'DISALLOW_FILE_EDIT', true );
// --- 2. Disable plugin/theme install and update from the dashboard ---
// Optional, stricter than the above. Use when you manage updates via CI,
// wp-cli, or a deploy pipeline and do not want dashboard installs.
// define( 'DISALLOW_FILE_MODS', true );
// --- 3. Force SSL for the admin and login flows ---
// If someone accesses /wp-admin over plain HTTP, cookies and credentials
// go over the wire in the clear. This constant rewrites to HTTPS.
define( 'FORCE_SSL_ADMIN', true );
// --- 4. Debug mode — OFF on production, always ---
// WP_DEBUG=true on a production site exposes file paths, SQL queries,
// and PHP warnings in the rendered HTML. It is a reconnaissance gift.
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_DEBUG_LOG', false );
// --- 5. Disable the script debugger ---
// Ensures minified JS/CSS are served, not the unminified debug versions.
define( 'SCRIPT_DEBUG', false );
// --- 6. Restrict post revisions ---
// Unlimited revisions bloat the wp_posts table. 5–10 is usually enough.
define( 'WP_POST_REVISIONS', 10 );
// --- 7. Empty trash on a reasonable schedule ---
// Default is 30 days. 7 days keeps the tables smaller.
define( 'EMPTY_TRASH_DAYS', 7 );
// --- 8. Disable the built-in cron in favor of system cron ---
// Requires a cron entry on the server. Worth it: wp-cron.php on every
// page load creates unpredictable load and breaks on low-traffic sites.
define( 'DISABLE_WP_CRON', true );
// --- 9. Control automatic updates ---
// Minor core updates on, major core updates via your deploy pipeline.
// (See the auto-updates section below for the rationale.)
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
define( 'AUTOMATIC_UPDATER_DISABLED', false );
// --- 10. Limit memory, but not too much ---
// The default is 40M, which is tight for modern plugin stacks.
// 256M is generous but not wasteful on typical shared hosting.
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // for admin only
// --- 11. Repair mode — OFF unless actively using it ---
// WP_ALLOW_REPAIR exposes /wp-admin/maint/repair.php to ANY visitor,
// authenticated or not. Leave it undefined except during a specific
// recovery operation, and unset it immediately afterwards.
// define( 'WP_ALLOW_REPAIR', true );
// --- 12. Harden cookie config ---
// Namespaces cookies per-site so two WP installs on subdomains do not
// share session state, and marks cookies httponly/secure where possible.
define( 'COOKIE_DOMAIN', '' ); // blank = per-host
define( 'COOKIEPATH', '/' );
define( 'SITECOOKIEPATH', '/' );
// --- 13. Enable filesystem method: direct ---
// Prevents WordPress from prompting for FTP credentials during updates
// (which would store them somewhere you do not want them stored).
define( 'FS_METHOD', 'direct' );
// --- 14. Custom table prefix ---
// Default is wp_. Custom prefix (set during install in wp-config) does
// not stop a determined attacker, but it does break automated exploit
// kits that assume wp_. Use a random 5-6 character suffix.
// $table_prefix = 'wp_9f3c_';
Salts and rotation
The eight authentication keys and salts in wp-config.php (`AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY`, and their `_SALT` counterparts) cryptographically bind WordPress cookies and nonces to your specific install. Rotating them invalidates every active session and every generated nonce, forcing a re-login for everyone.
That is useful in two situations: immediately after a suspected compromise, and on a scheduled rotation. We rotate keys quarterly for sites we manage, and immediately after any admin credential change.
# Fetch fresh keys from the official WordPress API
curl -s https://api.wordpress.org/secret-key/1.1/salt/
# Paste the output in place of the existing AUTH_KEY etc. block in wp-config.php.
# Every session is invalidated the moment you save the file. Do not generate these by hand. The API endpoint gives you cryptographically strong random values. A 10-character string you typed is not the same thing.
Restricting wp-login.php via .htaccess
The `/wp-login.php` endpoint is the single most-attacked URL on any WordPress site. Even on a tiny, low-traffic blog, expect thousands of brute-force attempts per day. Rate limiting helps. A plugin like Limit Login Attempts helps more. Restricting access at the web server level helps most.
If you have a fixed set of IPs that need admin access (your office, a VPN), add an `.htaccess` block. If not, the second-best option is to require HTTP basic auth in front of WordPress's own login. Attackers have to pass the basic auth before they can even see the WordPress login form, which cuts brute-force volume to effectively zero.
# .htaccess in the WordPress document root
# Option A: IP allowlist (replace with your actual IPs)
<Files wp-login.php>
Require ip 198.51.100.7
Require ip 203.0.113.0/24
</Files>
# Option B: HTTP basic auth wrapper
<Files wp-login.php>
AuthType Basic
AuthName "Restricted"
AuthUserFile /path/to/.htpasswd
Require valid-user
</Files> If you use xmlrpc.php (Jetpack, some mobile apps), restrict it similarly or disable it entirely if you do not. xmlrpc.php is the other workhorse endpoint for brute-force and amplification attacks.
Auto-updates: our actual policy
There is no single correct answer here. Our current policy, after trying the other options:
- · Minor core updates: on. These are security patches and rarely break anything. The risk of not applying them outweighs the risk of them shipping a regression.
- · Major core updates: off by default, on for sites under managed care where we test the release in staging first.
- · Plugin auto-updates: opt-in per plugin. We enable them for plugins with a strong track record (Yoast, WooCommerce core, official LiteSpeed Cache), and leave them off for anything with a history of regressions.
- · Theme auto-updates: off. Themes accumulate customizations and auto-updates frequently overwrite them.
The important thing is not the specific policy. It is that you picked one deliberately rather than accepting WordPress's defaults without reading them. Previously we had auto-updates fully enabled and lost a morning to a plugin update that conflicted with a WooCommerce beta on a customer's site. Now we stage, then update.
The validation script we run after
Checklist is only useful if applied consistently. This is the script we run against wp-config.php after provisioning, to confirm the hardening actually stuck:
#!/bin/bash
# wp-config-audit.sh <path-to-wp-config>
f="${1:-wp-config.php}"
check() {
local pattern="$1"
local label="$2"
if grep -qE "$pattern" "$f"; then
printf '[ OK ] %s\n' "$label"
else
printf '[FAIL] %s\n' "$label"
fi
}
check "DISALLOW_FILE_EDIT'?\s*,\s*true" "DISALLOW_FILE_EDIT = true"
check "FORCE_SSL_ADMIN'?\s*,\s*true" "FORCE_SSL_ADMIN = true"
check "WP_DEBUG'?\s*,\s*false" "WP_DEBUG = false"
check "DISABLE_WP_CRON'?\s*,\s*true" "DISABLE_WP_CRON = true"
check "FS_METHOD'?\s*,\s*'direct'" "FS_METHOD = direct"
check "\\$table_prefix\s*=\s*'wp_[a-z0-9]" "Custom table prefix"
# Warn if WP_ALLOW_REPAIR is left enabled
if grep -qE "WP_ALLOW_REPAIR'?\s*,\s*true" "$f"; then
printf '[WARN] WP_ALLOW_REPAIR is enabled — DISABLE after recovery\n'
fi
# Warn if file is world-readable
perms=$(stat -c %a "$f")
if [ "$perms" -gt 640 ]; then
printf '[WARN] wp-config.php permissions are %s (should be 640 or 600)\n' "$perms"
fi A hardening checklist is only a checklist. Turning it into a script you run on every site is the difference between intent and practice.
None of this is a replacement for keeping WordPress, plugins, and PHP updated. It is a baseline that raises the cost of the easiest attacks. Combined with our migration workflow and backup policy (linked in the guides), it is the minimum we consider safe to put a WordPress site in front of real traffic.
Omega Digital
Platform · Omega Digital