AWS Secrets Manager is a simple and powerful way to handle secrets (such as database username/password credentials). It provides support for storing, retrieving, managing, and rotating credentials at an affordable cost (currently $0.40 per secret per month). However, it’s not terribly easy to use with WordPress. I have not been able to find any documentation or samples for how to set up WordPress to use AWS Secrets Manager for its database access credentials, so I figured out how to do that and I’m sharing my findings here.
In the process, I encountered a few challenges:
- There is no (currently?) AWS Secrets Manager caching library for PHP. It’s important to use caching for secrets to reduce costs, reduce latency, and improve availability.
- Using the AWS SDK for PHP with WordPress would require using a composer based build process to namespace the SDK (to avoid conflict with other plugins) and ideally remove the unnecessary parts.
- There are no WordPress hooks for database access.
- Zero-downtime credential rotation is a requirement. I did not want to require the WordPress container/VM to have to be restarted when the credentials changed.
To get around these challenges, I decided to not use the AWS SDK for PHP and instead have PHP use exec to call the AWS CLI. For caching, the secret is written to a file in the system temp directory. When accessing the database, the credentials are read from the file. If login fails, the credentials are retrieved from AWS Secrets Manager again and the file is updated, then the connection is retried. This approach is similar to that used by the AWS Secrets Manager JDBC Library except instead of a file, it stores the information in memory. And finally, to get around WordPress’s lack of database access hooks, the wpdb global is overridden by placing a drop-in db.php file in the wp-content directory.
WP-CLI is another challenge. It always uses the DB_USER
and DB_PASSWORD
constants defined in wp-config.php
; it will not use the wp-content/db.php
drop in. Therefore, a WP-CLI specific file is necessary, wp-cli-secrets-manager.php
. Unlike the WordPress db.php
approach, however, this WP-CLI implementation always gets the secret and cannot cache it. The reason is that there is no WP-CLI hook or method that consistently is used to get the database connection or credentials, so there is no way to try potentially expired credentials then refresh them only if they do not work.
The Implementation
You must set two environment variables: AWS_DEFAULT_REGION
(must contain the region of the secret, for example, us-east-1
) and WORDPRESS_DB_SECRET_ID
(contains either the name of ARN of the secret). The secret must have a SecretString
containing a JSON object with username
and password
properties. Then place the follow file in wp-content/db.php:
<?php
if ( empty ( getenv('WORDPRESS_DB_SECRET_ID') ) ) {
// if the secret id isn't set, don't install this approach
return;
}
/*
Plugin Name: Extended wpdb to use AWS Secrets Manager credentials
Description: Get the database username/password from an AWS Secrets Manager
Version: 1.0
Author: Craig Andrews
*/
class wpdb_aws_secrets_manager_extended extends wpdb {
/**
* Path to the cache file
*
* @var string
*/
private $secretCacheFile;
public function __construct() {
$this->dbname = defined( 'DB_NAME' ) ? DB_NAME : '';
$this->dbhost = defined( 'DB_HOST' ) ? DB_HOST : '';
$this->secretCacheFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5(getenv('WORDPRESS_DB_SECRET_ID'));
$this->_load_credentials();
parent::__construct( $this->dbuser, $this->dbpassword, $this->dbname, $this->dbhost );
}
public function db_connect( $allow_bail = true ) {
$ret = parent::db_connect( false );
if (! $ret ) {
// connection failed, refresh the credentials
$this->_refresh_credentials();
$ret = parent::db_connect( $allow_bail );
}
return $ret;
}
/**
* Load the credentials from cached storage
* If no credentials are cached, refresh credentials
*/
private function _load_credentials() {
if ( file_exists ( $this->secretCacheFile ) ) {
$data = json_decode ( file_get_contents ( $this->secretCacheFile ) );
$this->dbuser = $data->username;
$this->dbpassword = $data->password;
} else {
$this->_refresh_credentials();
}
}
/**
* Refresh the credentials from Secrets Mananager
* and write to cached storage
*/
private function _refresh_credentials() {
exec('aws secretsmanager get-secret-value --secret-id ' . escapeshellarg(getenv('WORDPRESS_DB_SECRET_ID')) . ' --query SecretString --output text > ' . escapeshellarg($this->secretCacheFile), $retArr, $status);
chmod($this->secretCacheFile, 0600); // Read and write for owner, nothing for everybody else
if ( $status != 0 ) {
$this->bail("Could not refresh the AWS Secrets Manager secret");
die();
}
$this->_load_credentials();
}
}
global $wpdb;
$wpdb = new wpdb_aws_secrets_manager_extended();
For WP-CLI, create this file:
<?php
if ( empty ( getenv('WORDPRESS_DB_SECRET_ID') ) ) {
// if the secret id isn't set, don't install this approach
return;
}
class aws_secrets_manager_utility {
/**
* Database user name
*
* @var string
*/
public $dbuser;
/**
* Database password
*
* @var string
*/
public $dbpassword;
public function __construct() {
exec('aws secretsmanager get-secret-value --secret-id ' . escapeshellarg(getenv('WORDPRESS_DB_SECRET_ID')) . ' --query SecretString --output text', $retArr, $status);
if ( $status != 0 ) {
die("Could not retrieve the AWS Secrets Manager secret");
}else{
$data = json_decode ( $retArr[0] );
$this->dbuser = $data->username;
$this->dbpassword = $data->password;
}
}
}
$aws_secrets_manager_utility = new aws_secrets_manager_utility();
# These constants have to be defined here before WP-CLI loads wp-config.php
define('DB_USER', $aws_secrets_manager_utility->dbuser);
define('DB_PASSWORD', $aws_secrets_manager_utility->dbpassword);
Then invoke wp-cli using the --require=wp-cli-secrets-manager.php
argument.
In Action as Part of VersionPress On AWS
I’ve implemented full support for AWS Secrets Manager for WordPress’s database credentials (including automatic password rotation) in VersionPress on AWS. If you’d like to see a complete, real world example of how to configure AWS Secrets Manager including the rotation lambda, VPC support, security groups configuration, and more, please take a look at this commit in VersionPress on AWS.
Securing and Rotating WordPress Database Credentials with AWS Secrets Manager by Craig Andrews is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.