Hardening wp-config.php: What Matters

wp-config.php is the one file on your WordPress site that knows everything: your database credentials, your cryptographic salts, your debug behavior, and a set of switches that decide whether an attacker who gets one foot in the door can take the whole house. It is also the subject of more copy-pasted folklore than any other file in the WordPress ecosystem.

This post separates the settings that measurably reduce risk from the ones that make you feel busy. For each, you'll get the exact constant, why it matters, and how to check it's actually working — because a hardening step you can't verify is a hardening step you don't have.

First, protect the file itself

Before tuning what's inside wp-config.php, make sure the file can't be read from the outside. Two failure modes matter:

  • Misconfigured PHP handling. If your web server ever serves PHP files as plain text — during a botched migration, after a server rebuild, when a .htaccess file goes missing — wp-config.php leaks your database password in one request. This isn't hypothetical; it's a classic post-migration incident.
  • Backup droppings. Files like wp-config.php.bak, wp-config.old, or wp-config.php~ (an editor's autosave) are served as plain text by default, because the server doesn't recognize them as PHP. Attackers request these names constantly. Check your web root right now: ls -la wp-config*. If there's more than one file, delete the extras.

On Apache, you can add an explicit deny block as a belt-and-braces measure. On nginx, a location rule that denies access to wp-config anything does the same. But the more durable fix is behavioral: never edit wp-config.php by making a copy next to it. Copy it outside the web root, or use version control.

Should you move wp-config.php above the web root?

WordPress will automatically look one directory up if wp-config.php isn't in the web root. Moving it protects against exactly one scenario: the server serving raw PHP source from the web root. That scenario is real but rare, and moving the file confuses some tooling (deploy scripts, some backup plugins, WP-CLI in odd setups). Verdict: worth doing on a hand-managed VPS where you control everything; skip it on managed hosting, where the host has usually solved raw-source leaks at the platform level and the nonstandard location just creates support friction.

The settings that genuinely matter

1. Unique security keys and salts

The eight AUTH_KEY / SECURE_AUTH_KEY / LOGGED_IN_KEY / NONCE_KEY constants (and their _SALT partners) feed WordPress's cookie signing and nonce generation. If they're weak, guessable, or — worst case — the literal placeholder text put your unique phrase here, an attacker who obtains a database dump or intercepts traffic has a far easier time forging authentication cookies.

Two practical rules:

  • Generate them from the official generator (api.wordpress.org/secret-key/1.1/salt/) or with wp config shuffle-salts via WP-CLI. Sixty-plus characters of real entropy each.
  • Rotate them after any suspected compromise. Rotating salts invalidates every login cookie on the site instantly — every session, every device, including the attacker's. It's the single fastest way to force a global logout, and it takes ten seconds.

Verification: open wp-config.php and confirm no key contains the placeholder phrase and no two keys are identical. You'd be surprised.

2. DISALLOW_FILE_EDIT — remove the built-in code editor

define( 'DISALLOW_FILE_EDIT', true );

By default, any administrator can edit theme and plugin PHP directly from the dashboard. That means a stolen admin cookie or a phished admin password converts instantly into arbitrary code execution — the attacker pastes a backdoor into your theme's functions.php from the comfort of wp-admin. This constant removes the editor entirely.

The objection is always 'but sometimes I need to make a quick edit.' You don't — you need SFTP or a proper deployment flow, and the two minutes that costs you is the price of removing the most convenient escalation path in WordPress. This is the highest value-per-character line in the file. If you set nothing else from this post, set this.

3. DISALLOW_FILE_MODS — for sites with a deployment pipeline

define( 'DISALLOW_FILE_MODS', true );

This goes further: it blocks installing, updating, or deleting plugins and themes from the dashboard entirely, and it implies DISALLOW_FILE_EDIT. On a site where all code changes flow through git and a deploy script, this is excellent — the dashboard becomes a content tool, not a code-distribution mechanism, and a compromised admin account can't install a malicious plugin.

On a site where updates happen through the dashboard (which is most small-business sites), do not set this — it will silently block security updates, which is a net loss. Decision rule: if you have a deploy pipeline, set it; if wp-admin is your deploy pipeline, don't.

4. Debug settings that don't leak

The dangerous default isn't WP_DEBUG itself — it's where the output goes. The safe production configuration is:

  • define( 'WP_DEBUG', false ); in normal operation.
  • When you must debug production, use define( 'WP_DEBUG', true ); together with define( 'WP_DEBUG_DISPLAY', false ); and define( 'WP_DEBUG_LOG', '/path/outside/webroot/debug.log' );

Two traps. First, WP_DEBUG_DISPLAY defaulting to true means errors — including full file paths and sometimes query fragments — print into pages for everyone to read. Second, the default log location, wp-content/debug.log, is inside the web root and publicly downloadable. Attackers request that URL routinely. Passing a path to WP_DEBUG_LOG (documented in the WordPress debugging guide) puts the log somewhere the web server can't serve.

5. FORCE_SSL_ADMIN

define( 'FORCE_SSL_ADMIN', true );

If your site runs on HTTPS (it does, or you have a different blog post to read first), this ensures login and admin traffic can't quietly downgrade to plain HTTP, where cookies travel unencrypted. On a fully HTTPS site with proper redirects this is mostly redundant — but it's a one-line backstop against redirect misconfiguration, and backstops are the point of hardening.

6. WP_ENVIRONMENT_TYPE — make environments self-aware

define( 'WP_ENVIRONMENT_TYPE', 'production' );

Not a security control by itself, but it enables them. Well-behaved plugins check wp_get_environment_type() to decide whether to send real emails, hit live payment APIs, or show debug UI. Setting this to 'staging' on your staging site and 'production' on the live one is how you prevent the classic incident where a staging copy emails 4,000 real customers. It also gives your own code a clean switch: loosen debug on staging, lock it down on production, all from one constant.

The folklore section: settings that don't do much

SettingThe claimThe reality
Custom table prefixStops SQL injectionAny SQL injection worth the name reads the prefix from information_schema in one query. Changing wp_ to xy7_ stops only the dumbest fully-automated scripts. Not worth a migration on an existing site; harmless to set on a new one.
Hiding the WordPress versionPrevents targeted attacksAttackers don't check your version and politely move on — they fire every exploit they have at every site. Patching is the defense; hiding the version number is theater.
Renaming wp-login.php via config tricksStops brute forceLogin endpoints are discoverable through the REST API and XML-RPC anyway. Rate limiting and strong passwords (or better, two-factor) do the actual work.

None of these actively hurt (except the migration risk of changing a live site's table prefix), but every minute spent on them is a minute not spent on the constants above, offsite backups, or update discipline — all of which have real payoff.

Database credentials: scope beats secrecy

The DB_USER in wp-config.php should be a user that can touch only this site's database. On shared or self-managed servers it's common to find one MySQL user with rights over every database on the box — which means one compromised site compromises all of them. Create one user per site, grant it privileges on that site's database alone, and skip global grants like SUPER or FILE, which WordPress never needs. The FILE privilege in particular turns an SQL injection into a file-write primitive — that's how injections become shells.

Also set DB_HOST as tightly as your setup allows. If MySQL is local, localhost (socket) is better than 127.0.0.1, and either is better than a wildcard-bound network listener.

File permissions in one paragraph

wp-config.php should be readable by the PHP process and nobody else who doesn't need it. On a typical single-site server, 640 (owner read/write, group read) or 600 is right, with ownership set so the web server can read it. The scary permission is world-readable on a shared server where other tenants have shell access — that's your database password on a bulletin board. If you're on managed hosting, the platform sets this; verify once with stat -c '%a' wp-config.php and move on. Chasing permission perfection beyond this point has sharply diminishing returns.

A production-ready reference block

Pulling it together, the hardening section of a production wp-config.php looks like this:

  1. Fresh, unique keys and salts (all eight, from the generator).
  2. define( 'DISALLOW_FILE_EDIT', true );
  3. define( 'DISALLOW_FILE_MODS', true ); — only if deploys don't go through wp-admin.
  4. define( 'WP_DEBUG', false ); with the DISPLAY/LOG pattern ready to swap in when needed.
  5. define( 'FORCE_SSL_ADMIN', true );
  6. define( 'WP_ENVIRONMENT_TYPE', 'production' );
  7. A per-site database user with single-database privileges.
  8. File mode 640 or 600, no stray wp-config backup copies in the web root.

Every item on that list is verifiable in under a minute, which is exactly what you want from a checklist: run through it quarterly, or any time a site changes hands.

The honest conclusion

wp-config.php hardening won't save a site with an unpatched plugin holding a known remote-code-execution bug — updates and backups still carry most of the security load. What these settings do is contain damage: they turn 'attacker got an admin password' from a full server compromise into a recoverable bad day, and they close the quiet leaks — debug logs, backup copies, over-privileged database users — that turn small mistakes into big ones. That containment is cheap. Eight lines and a permissions check, verified once a quarter. Do the boring version, skip the folklore, and spend the time you saved on your backup restore test — which, conveniently, is another post.

Photo: cottonbro studio on Pexels