Storing credentials in a Lambda with Secrets Manager (AWS CDK)

Danny Hines

Danny Hines • 4 min read

Posted Aug 12 2021

Secrets Manager

In most cases you don't want to place highly sensitive information, like database credentials or API keys, in the application code itself. This is problematic because they're viewable to anyone examining the code, and updating the credentials requires updating & deploying a new version of the application.

AWS Secrets Manager lets you to replace hardcoded credentials with an API call to retrieve them programmatically. It also lets you rotate the secrets according to a specified schedule which significantly reduces the risk of compromise.

Setup

First add your secrets to AWS Secret Manager. Later we'll use the ARN in our lambda to retrieve these values:

Secrets Manager

Create a CDK project

If you don't have the CDK CLI installed:

npm install -g aws-cdk
bash

If you haven't done so already, login and set your AWS credentials with aws configure.

Then use the CLI to initialize a new project:

cdk init
bash

Add the ARN to .env

Create a .env file at the root of your project and copy the ARN from the secret you created. This will be passed to the env variables within the lambda.

MY_SECRET_ARN='arn:aws:secretsmanager:...'
text

The Stack

The goal of this project is to store credentials in AWS Secretes Manager and use them in a Lambda function. To keep it simple, we're going to create a Cloudwatch event to trigger our function on a schedule. The Lambda will then use the credentials for something super duper secret (connect to a database, use an external API, etc.).

architecture diagram of lambda and secrets manager

Here's the file for creating our resources (this belongs in the lib/ folder):

require('dotenv').config(); import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration } from '@aws-cdk/core'; import * as targets from '@aws-cdk/aws-events-targets'; import * as iam from '@aws-cdk/aws-iam'; import * as events from '@aws-cdk/aws-events'; export class LambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const { MY_SECRET_ARN } = process.env; // LAMBDA FUNCTION const lambdaFunction = new lambda.Function(this, 'lambda-function', { code: new lambda.AssetCode('lambda', { // assumes lambda code is in ./lambda/ exclude: ['*.ts', '*.d.ts', 'layer', 'node_modules'], }), handler: 'index.handler', runtime: lambda.Runtime.NODEJS_14_X, functionName: 'FunctionName', timeout: Duration.seconds(60), environment: { MY_SECRET_ARN: MY_SECRET_ARN || '', // other env variables for the lambda }, }); lambdaFunction.addToRolePolicy( new iam.PolicyStatement({ resources: [MY_SECRET_ARN || ''], actions: ['secretsmanager:GetSecretValue'], }) ); // CRON JOB const rule = new events.Rule(this, 'cron-job', { schedule: events.Schedule.cron({ minute: '0/10' }), // every 10 minutes }); rule.addTarget(new targets.LambdaFunction(lambdaFunction)); } }
typescript

This will provision and deploy a lambda that has access to retrieve secrets from the specified ARN. The function will be triggered on a cron job using Cloudwatch events (this function will run every 10 minutes).

Creating the lambda

The lambda code will live at the root of the project. Also install dotenv and the aws-sdk for retrieving your credentials:

mkdir lambda cd lambda npm init npm install aws-cdk dotenv
bash

Then in your lambda code, retrieve the credentials using the ARN, which is passed to the lambda's environment variables.

For example, in one of my apps I use this strategy with a NodeJS Twitter library to create a "Client" using the consumer_key and consumer_secret that's stored in Secrets Manager. The client object is then used to listen for tweets, search, etc.

require('dotenv').config(); import { SecretsManager } from 'aws-sdk'; const { TwitterApi } from 'twitter-api-v2'; const getTwitterClient = async () => { const secretsManager = new SecretsManager(); const result = await secretsManager .getSecretValue({ SecretId: process.env.MY_SECRET_ARN }) .promise(); const { consumer_key, consumer_secret } = JSON.parse(result.SecretString!); const twitterApi = new TwitterApi({appKey: consumer_key, appSecret: consumer_secret}); const client = twitterApi.readOnly; return client; };
ts

Continuing the example, this is how I use the Twitter API client:

try { const client = await getTwitterClient(); const params = { 'user.fields': 'username', expansions: 'author_id', 'tweet.fields': 'created_at', }; const tweets = await client.v2.search('from:elonmusk -is:retweet', params); console.log(tweets); } catch (error) { console.log(error); }
ts

That's it! Now your secrets can't be compromised by someone examining your code, because the secret no longer exists in the code. You can also add rotation schedules using the CDK or in the console under Rotation configuration.