Retrieving Database Credentials from AWS Secrets Manager with CFML
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:
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.