Mautic & GCP: Automate Database Password Rotation with Secrets Manager
For a Mautic instance hosted on Google Cloud Platform (GCP), automating database credential rotation with Google Cloud Secret Manager introduces a common operational challenge: Mautic reads its database configuration from app/config/local.php on every request or CLI cron execution, and hardcoding static credentials breaks the moment Cloud SQL rotates the password.
Conversely, calling the Secret Manager API on every single HTTP request adds unnecessary network latency and API costs. Below is a production-ready pattern that dynamically fetches rotated credentials from Google Cloud Secret Manager and caches them locally with strict file permissions and a configurable TTL.
Prerequisites & IAM Setup
- Compute / GKE Workload Identity: Ensure the VM service account running Mautic has the Secret Manager Secret Accessor (
roles/secretmanager.secretAccessor) IAM role bound only to the specific database secret. - Composer Dependency: Install the official Google Cloud Secret Manager PHP client inside your Mautic root directory:
composer require google/cloud-secret-manager
Updating app/config/local.php with TTL Caching
Replace the static database password in app/config/local.php with the following implementation. It checks a local JSON cache file first (with a 1-hour TTL) and falls back to querying versions/latest from Google Cloud Secret Manager when the cache expires:
<?php
use Google\Cloud\SecretManager\V1\SecretManagerServiceClient;
use Google\ApiCore\ApiException;
// Use a static variable to ensure the configuration is only processed once per request.
static $mautic_parameters_config = null;
if (null === $mautic_parameters_config) {
// Define your project and secret details here.
define('GCP_PROJECT_ID', 'your-gcp-project');
define('SECRET_DB_PASSWORD', 'your-secret-for-password');
// Store the credential cache outside public web roots with restrictive permissions.
define('CREDENTIALS_CACHE_FILE', __DIR__ . '/../cache/prod/db_credentials.json');
// Cache lifetime in seconds (e.g., 3600 = 1 hour).
define('CACHE_TTL_SECONDS', 3600);
$dbCredentials = null;
// Read credentials from the local cache if still within TTL.
if (file_exists(CREDENTIALS_CACHE_FILE) && (time() - filemtime(CREDENTIALS_CACHE_FILE) < CACHE_TTL_SECONDS)) {
$cachedData = file_get_contents(CREDENTIALS_CACHE_FILE);
if ($cachedData !== false) {
$decoded = json_decode($cachedData, true);
if (is_array($decoded) && !empty($decoded['password'])) {
$dbCredentials = $decoded;
}
}
}
// If the cache is expired, missing, or invalid, fetch from GCP Secret Manager.
if (!$dbCredentials) {
try {
require_once __DIR__ . '/../vendor/autoload.php';
$client = new SecretManagerServiceClient();
function getSecretValue(SecretManagerServiceClient $client, string $projectId, string $secretId): string {
$name = $client->secretVersionName($projectId, $secretId, 'latest');
$response = $client->accessSecretVersion($name);
return $response->getPayload()->getData();
}
$dbCredentials = [
'password' => getSecretValue($client, GCP_PROJECT_ID, SECRET_DB_PASSWORD),
];
$cacheDir = dirname(CREDENTIALS_CACHE_FILE);
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0700, true);
}
file_put_contents(CREDENTIALS_CACHE_FILE, json_encode($dbCredentials), LOCK_EX);
chmod(CREDENTIALS_CACHE_FILE, 0600);
} catch (ApiException | Exception $e) {
error_log('ERROR: Could not fetch credentials from Secret Manager: ' . $e->getMessage());
$dbCredentials = [
'host' => 'FETCH_FAILED',
'name' => 'FETCH_FAILED',
'user' => 'FETCH_FAILED',
'password' => 'FETCH_FAILED',
];
}
}
// Build the final Mautic parameters array.
$mautic_parameters_config = [
'db_password' => $dbCredentials['password'],
'db_host' => 'localhost',
'db_name' => 'dbname',
'db_user' => 'username',
'db_driver' => 'pdo_mysql',
'db_host_ro' => null,
'db_table_prefix' => null,
'db_port' => '3306',
'db_backup_tables' => 1,
'db_backup_prefix' => 'bak_',
'secret_key' => 'replace-with-your-mautic-secret-key',
'site_url' => 'https://mautic.example.com',
];
}
$parameters = $mautic_parameters_config;
Handling Zero-Downtime Rotation
When rotating passwords in Cloud SQL or MySQL:
- Dual-User or Staggered Rotation: If your rotation Cloud Function updates the password in-place, delete
db_credentials.json(or runphp bin/console cache:clear) immediately after rotation so Mautic fetches the new secret version on the very next request without waiting forCACHE_TTL_SECONDSto expire. - File Permissions & Serialization Safety: Always use
json_encode()/json_decode()rather thanserialize()/unserialize()for cached data, and enforce0600file permissions so only thewww-data/ PHP-FPM user can read the cached secret.
Related Articles
PHP script timed out before returning headers
Directadmin throws an error: suPHP error script timed out before returning headers
Custom Apache Handler for Running Multiple PHP Versions Per VirtualHost
How to run multiple PHP versions side-by-side in Apache VirtualHosts using PHP-FPM Unix sockets (Apache 2.4+) or CGI ScriptAlias handlers.
Ghostty Transparency
How to set transparency for Ghostty