WordPress Secrets Management: Vault, Doppler, and Env-Based Config

Hardcoding API keys and database passwords in wp-config.php — even with restrictive file permissions — creates a single point of secret compromise: anyone with read access to the file system has all credentials. A secrets manager (HashiCorp Vault, Doppler, AWS Secrets Manager, or 1Password Secrets Automation) rotates, audits, and injects secrets at runtime, so your repository and filesystem never contain production credentials.

Problem: WordPress wp-config.php stores API keys, database credentials, and payment gateway secrets in plain text — these leak into version control, server backups, and staging environments where they should not be accessible.

Solution: Use an external secrets manager: integrate with HashiCorp Vault via the Vault PHP SDK, fetch secrets at runtime with $client->read('secret/wordpress/stripe'), and cache them in a short-lived transient. For simpler setups, use Doppler or AWS Secrets Manager with a PHP SDK and store only the authentication token in wp-config.php, not the secrets themselves. Never commit secrets to Git.


The code below shows three patterns: environment variable injection from Doppler via a wp-config.php that reads from $_ENV, a HashiCorp Vault AppRole fetch in a WP-CLI command, and a wp-config.php pattern that pulls secrets from AWS Secrets Manager at bootstrap.


<?php
// ── Pattern 1: Environment variables (Doppler / Docker / systemd env) ────
// wp-config.php — never hardcode; read from environment
define( 'DB_NAME',     getenv( 'WORDPRESS_DB_NAME' )     ?: $_ENV['WORDPRESS_DB_NAME']     ?? '' );
define( 'DB_USER',     getenv( 'WORDPRESS_DB_USER' )     ?: $_ENV['WORDPRESS_DB_USER']     ?? '' );
define( 'DB_PASSWORD', getenv( 'WORDPRESS_DB_PASSWORD' ) ?: $_ENV['WORDPRESS_DB_PASSWORD'] ?? '' );
define( 'DB_HOST',     getenv( 'WORDPRESS_DB_HOST' )     ?: '127.0.0.1' );

// Auth keys from environment (generate with: wp secret generate)
define( 'AUTH_KEY',        getenv( 'WP_AUTH_KEY' )        ?: '' );
define( 'SECURE_AUTH_KEY', getenv( 'WP_SECURE_AUTH_KEY' ) ?: '' );
// ... etc

// ── Pattern 2: HashiCorp Vault AppRole (fetched at runtime) ────────────
function wp_vault_get_secret( string $path, string $field ): string {
    $cache_key = 'vault_' . md5( $path . $field );
    $cached    = wp_cache_get( $cache_key, 'vault' );
    if ( false !== $cached ) {
        return (string) $cached;
    }

    // AppRole login
    $vault_addr = getenv( 'VAULT_ADDR' ) ?: 'https://vault.example.com';
    $role_id    = getenv( 'VAULT_ROLE_ID' )   ?: '';
    $secret_id  = getenv( 'VAULT_SECRET_ID' ) ?: '';

    $login = wp_remote_post( "$vault_addr/v1/auth/approle/login", [
        'body'    => wp_json_encode( [ 'role_id' => $role_id, 'secret_id' => $secret_id ] ),
        'headers' => [ 'Content-Type' => 'application/json' ],
        'timeout' => 5,
    ] );
    if ( is_wp_error( $login ) ) {
        wp_die( 'Vault authentication failed.' );
    }
    $token = json_decode( wp_remote_retrieve_body( $login ), true )['auth']['client_token'] ?? '';

    // Read secret
    $secret_response = wp_remote_get( "$vault_addr/v1/secret/data/$path", [
        'headers' => [ 'X-Vault-Token' => $token ],
        'timeout' => 5,
    ] );
    $value = json_decode( wp_remote_retrieve_body( $secret_response ), true )['data']['data'][ $field ] ?? '';

    wp_cache_set( $cache_key, $value, 'vault', 300 );  // cache 5 minutes
    return (string) $value;
}

// Usage in wp-config.php (before WordPress loads):
// define( 'DB_PASSWORD', wp_vault_get_secret( 'wordpress/prod', 'db_password' ) );


# ── Doppler: inject secrets as env vars at process start ────────────
# Install Doppler CLI
brew install dopplerhq/cli/doppler

# Authenticate
doppler login
doppler setup  # link to your Doppler project and config

# Run WP-CLI with Doppler-injected secrets
doppler run -- wp core install --url=https://example.com ...

# Run PHP-FPM with Doppler secrets (systemd service override)
# /etc/systemd/system/php8.2-fpm.service.d/doppler.conf:
# [Service]
# ExecStart=
# ExecStart=/usr/bin/doppler run -- /usr/sbin/php-fpm8.2 --nodaemonize


NOTE: Fetching secrets from Vault or AWS Secrets Manager in wp-config.php on every request adds 5–50ms of network latency — always cache the result in PHP's apcu_store() (within a single FPM process lifecycle) or in the WordPress object cache with a short TTL, and ensure your secrets manager endpoint is in the same VPC/datacenter as the WordPress server to minimise latency.