Securing and Rotating WordPress Database Credentials with AWS Secrets Manager

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:

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.

CC BY-SA 4.0 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.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.