Matthew J. Clemente

Retrieving Database Credentials from AWS Secrets Manager with CFML

Jan 17, 2022
5 minutes

As a quick follow-up to my last post about storing database credentials in AWS Secrets Manager, I wanted to walk through retrieving them using CFML, so you can actually use them in your application.

IAM User and Permissions

The first thing we'll need is AWS credentials - specifically, the access key ID and secret key of an IAM user with permissions to retrieve a secret. AWS provides documentation on the permissions that are needed:

  • secretsmanager:ListSecrets
  • secretsmanager:DescribeSecret
  • secretsmanager:GetSecretValue

Setting up IAM users and policies is outside the scope of this post - pretty sure more than one book has been written on the topic. Once you've got a user with the correct permissions, we can move on the code.

aws-cfml

We'll use @jcberquist's package aws-cfml to handle authentication with AWS and the actual secret retrieval. You can use CommandBox to install it in your project:

box install aws-cfml

This will download the project and install it in the directory modules/awscfml/.

If you're not comfortable using Commandbox, you can download the project directly from Github and manually load it into your project.

Loading AWS Credentials

My preferred method of providing aws-cfml with the AWS credentials is via environment variables. The project will automatically recognize and use environment variables named AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_DEFAULT_REGION.

Alternatively, you can provide the credentials at initialization:

aws = new modules.awscfml.aws( awsKey = 'YOUR_PUBLIC_KEY', awsSecretKey = 'YOUR_PRIVATE_KEY', defaultRegion = 'us-east-1' );

Secret Retrieval

With our credentials provisioned and aws-cfml installed, we're ready to actually make the request to Secrets Manager for our database credentials secret.

Here's how we'd use aws-cfml to retrieve a database credential secret named /path/to/secret/db_access:

// init assumes your credentials are set as envs
aws = new modules.awscfml.aws();

secret_response = aws.secretsmanager.GetSecretValue( '/path/to/secret/db_access' );
db_secret_value = secret_response.data.SecretString;

// comes back as JSON string, so we need to deserialize
db_access = deserializeJSON(db_secret_value);

writeDump( '#db_access#' );

The result, in this example, would be a struct containing keys for username, password, engine, host, port, and dbname. It would look something like this:

CFML struct with AWS Secrets Manager database credentials

So, how would you actually use this to populate a datasource in your Application.cfc?

Application.cfc Structure

Storing database credentials in a remote secret manager comes with a couple of puzzles. To begin with, how do we define a datasource with the credentials? To handle this, we'll set up a function in Application.cfc that abstracts away the logic of requesting the information from AWS Secrets manager:

this.datasource = 'appdsn_exactoritos';
this.datasources["appdsn_exactoritos"] = getDatabaseConfig();

The getDatabaseConfig function encapsulates the use of aws-cfml, making the request to AWS for the database credentials secret, and then using it to build a datasource struct, which it will then return. Note that the struct will look slightly different, depending on if you're using Adobe ColdFusion or Lucee.

But before we write out this code, there's another problem. We're making an HTTP request to AWS to retrieve the credentials, but we won't want that overhead on every request. So, how do we persist the database credentials, so that we don't need to keep making HTTP requests?

Persisting Credentials

When I first encountered this problem, my initial thought was to save the credentials to the application scope. Unfortunately, within getDatabaseConfig the application scope doesn't exist yet. The solution I settled on was using Java system properties.

Here's how it works: we first check for a system property that indicates if the datasource secret has been retrieved yet. This can be called something like com.app.db_is_configured.

var sys = CreateObject("java", "java.lang.System");
var db_is_configured = sys.getProperty("com.app.db_is_configured");

If this is null, we know that we need to make a request to AWS to retrieve the secret.

if( isNull( db_is_configured ) ) {
var aws = new modules.awscfml.aws();
// retrieve credential secret

Once we've got the database credentials, we can store them as system properties; one for the host, one for the port, etc:

sys.setProperty("com.app.db_host", db_access.host );
sys.setProperty("com.app.db_port", db_access.port );
sys.setProperty("com.app.db_name", db_access.dbname );
sys.setProperty("com.app.db_username", db_access.username );
sys.setProperty("com.app.db_password", db_access.password );
sys.setProperty("com.app.db_connection_string", "jdbc:postgresql://#db_access.host#:#db_access.port#/#db_access.dbname#" );

We then set the system property com.app.db_is_configured to true, so that we know that we don't need to make the request to AWS again.

Finally, we return the datasource struct, using the Java system properties that we just set:

// this example uses the Lucee datasource syntax
return {
class: 'org.postgresql.Driver'
, bundleName: 'org.postgresql.jdbc'
, bundleVersion: '42.2.20'
, connectionString: sys.getProperty("com.app.db_connection_string")
, username: sys.getProperty("com.app.db_username")
, password: sys.getProperty("com.app.db_password"
};

Putting it all together

Putting it all together, we can define our datasource like this:

this.datasource = 'appdsn_exactoritos';
this.datasources["appdsn_exactoritos"] = getDatabaseConfig();

private struct function getDatabaseConfig() {
var sys = CreateObject("java", "java.lang.System");
var db_is_configured = sys.getProperty("com.app.db_is_configured");

if( isNull( db_is_configured ) ) {
var aws = new modules.awscfml.aws();

var secret_response = aws.secretsmanager.GetSecretValue( '/path/to/secret/db_access' );
var db_secret_value = secret_response.data.SecretString;

var db_access = deserializeJSON(db_secret_value);

sys.setProperty("com.app.db_host", db_access.host );
sys.setProperty("com.app.db_port", db_access.port );
sys.setProperty("com.app.db_name", db_access.dbname );
sys.setProperty("com.app.db_username", db_access.username );
sys.setProperty("com.app.db_password", db_access.password );
sys.setProperty("com.app.db_connection_string", "jdbc:postgresql://#db_access.host#:#db_access.port#/#db_access.dbname#" );

sys.setProperty("com.app.db_is_configured", true );
}
return {
class: 'org.postgresql.Driver'
, bundleName: 'org.postgresql.jdbc'
, bundleVersion: '42.2.20'
, connectionString: sys.getProperty("com.app.db_connection_string")
, username: sys.getProperty("com.app.db_username")
, password: sys.getProperty("com.app.db_password")
};
}

And here's a link to that code as a gist so you can more easily copy, fork, etc.

Hope this helps! And if there's anything that I left out or if you have any questions, please let me know in the comments.