Technote

Security Practical

Series From local to production Part 3 of 8

wp-config and secrets: keeping per-environment values out of the repo

A committed secret is a leaked secret. Rewriting history does not take it back, and the only remaining fix is rotation.

wp-config.php is the one file in WordPress whose contents must differ per environment. Database credentials, authentication salts, debug switches and cache settings live there, and every one of them differs between local, staging and production. That is why the file cannot be in the repository: commit one version and you have made the others wrong.

Two workable places for environment values

Two arrangements hold up in practice, and both follow the same principle: the repository states what is required, never what the value is.

Where values live — the repository keeps the shape, the environment keeps the value

The first option has wp-config.php read an untracked file, with only a sample of that file committed. The second reads environment variables, which suits containers and, under PHP-FPM, is injected through env[] in the pool configuration.

<?php
// wp-config.php — never committed.

$local = __DIR__ . '/wp-config-local.php';
if ( is_readable( $local ) ) {
    require $local;          // DB_*, salts, other secrets
}

$env = getenv( 'WP_ENV' ) ?: 'production';

define( 'WP_ENVIRONMENT_TYPE', $env );
define( 'WP_DEBUG',           'production' !== $env );
define( 'WP_DEBUG_DISPLAY',   false );
define( 'DISALLOW_FILE_EDIT', true );

// ⚠ every definition above must sit above this line.
require_once ABSPATH . 'wp-settings.php';

That last comment is the thing people trip over most. Constants must be defined before wp-settings.php is required. Put them after and WordPress has already booted, so they are silently ignored — no error, no warning, the setting simply does not apply.

WP_ENVIRONMENT_TYPE is a constant WordPress reads itself. In code you read it back through wp_get_environment_type(), and the permitted values are local, development, staging and production. The staging safeguards in part five are built on it.

Do not hand-edit the values

Editing the config by hand when standing up a new environment invites typos and omissions. WP-CLI does the same job as a command.

wp config create --dbname=wp --dbuser=wp --dbpass=... --skip-check
wp config set WP_ENVIRONMENT_TYPE staging
wp config set WP_DEBUG true --raw     # write it unquoted
wp config get DB_NAME
wp config shuffle-salts               # issue eight fresh auth keys

Without --raw, true is written as a string. The string "true" happens to be truthy, so WP_DEBUG works by accident — but a constant expecting a number will not. It is worth forming the habit correctly.

A committed secret is a leaked secret

Mistakes happen; what matters is what you do next. Start by finding out whether a secret is in the history at all.

git log -p -S 'DB_PASSWORD' --all -- wp-config.php

If that returns anything, the value has to be replaced. History-rewriting tools exist, but they only tidy the repository — the clones, forks, CI logs, backups and screenshots that already exist do not come back. When you cannot control where a value went, assuming it went everywhere is the only safe premise.

Handling a committed secret — skip the second box and the rest is just reassurance

Rotate the database password, API keys, webhook keys and the authentication salts. Changing salts logs everybody out and forces a fresh login, and that is precisely the point — it invalidates any stolen cookie. Finally, check permissions: the web server only needs to read wp-config.php, so 640 is enough.

Secrets handling and server hardening continue in the Security archive, and if you would like a live site’s configuration, permissions and vulnerabilities reviewed in one pass, that diagnostic is part of our optimization program.

Next part

Code and configuration are sorted; data is what remains. The next part covers the most widely misunderstood topic in this series — database sync — and why replacing a domain with SQL makes settings vanish.

More on this topic

All technotes

Security Practical

The first hour after you discover a compromise

The most common mistake in this moment is deleting things in a hurry. Here is what to stop first, what to preserve, and in what order to recover…

Founders 8 min read

₩270,000 · Join the program