DEV Community

Wesley Cheek
Wesley Cheek

Posted on

Securely Use Secrets In Your AWS CDK Deployed Lambda Function

Have you ever needed secure access to things like login details in your Lambda functions? You might need to connect to a database or other service, but you don’t want to need to hard-code your credentials. Well, let’s use best security practices by getting our login details using AWS Secrets Manager!

I will be using Python flavored AWS CDK to deploy a Lambda function which has permissions to access a secret stored in Secrets Manager. I will also show you how to retrieve this secret securely inside your lambda function.

See the GitHub repository here.

Before getting into the CDK components, let’s create an example secret using AWS CLI:

aws secretsmanager create-secret --name secretsExample --secret-string "TestPass123"
Enter fullscreen mode Exit fullscreen mode

Stack Design

cdk_lambda_secrets/cdk_lambda_secrets_stack.py

Here we build the CDK stack by creating a lambda function and giving it permission to read our secret. We can pass the name of the secret into the lambda function as an environment variable to save ourselves some repetition.

import os

from aws_cdk import Stack
from aws_cdk import aws_lambda as _lambda
from aws_cdk import aws_secretsmanager as secrets
from constructs import Construct


class CdkLambdaSecretsStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        self.build_lambda_func()

    def build_lambda_func(self):
        secret_name = "secretsExample"
        self.secrets_lambda = _lambda.Function(
            scope=self,
            id="LambdaWithSecrets",
            runtime=_lambda.Runtime.PYTHON_3_9,
            function_name="LambdaWithSecretsExample",
            code=_lambda.Code.from_asset(
                path="lambda_funcs/lambda_with_secrets"
            ),
            handler="lambda_with_secrets.handler",
            # We need these env vars to access the secret inside the lambda
            environment={
                "secret_name": secret_name,
                "secret_region": os.environ["CDK_DEFAULT_REGION"],
            },
        )
        # Grant permission to the Lambda func to access the secret
        example_secret = secrets.Secret.from_secret_name_v2(
            scope=self, id="secretExample", secret_name=secret_name
        )
        example_secret.grant_read(grantee=self.secrets_lambda)

Enter fullscreen mode Exit fullscreen mode

Lambda Function

lambda_funcs/lambda_with_secrets/lambda_with_secrets.py

Here we define a lambda function with a helper function get_secrets(). This is a very slightly modified version of the ‘Sample code’ function suggested by AWS when you look at the secret in the AWS Console. Rather than use the ARN to access the secret, we can simply use the name of the secret. I found that I was unable to get the full ARN of the secret using CDK, but luckily get_secret_value() accepts the secret name for its SecretId parameter.

import base64
import json
import os

import boto3
from botocore.exceptions import ClientError


def get_secret():
    """
    Gets the secret
    """
    # Get env vars we passed in the stack.
    secret_name = os.getenv("secret_name")
    region_name = os.getenv("secret_region")

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name="secretsmanager", region_name=region_name
    )

    # In this sample we only handle the specific exceptions for the 'GetSecretValue' API.
    # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
    # We rethrow the exception by default.

    try:
        # We need only the name of the secret
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "DecryptionFailureException":
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InternalServiceErrorException":
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InvalidParameterException":
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InvalidRequestException":
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "ResourceNotFoundException":
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        else:
            raise e
    else:
        # Decrypts secret using the associated KMS key.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if "SecretString" in get_secret_value_response:
            secret = get_secret_value_response["SecretString"]
            # If you have multiple secret values, you will need to json.loads(secret) here and then access the values using dict keys
            return secret
        else:
            decoded_binary_secret = base64.b64decode(
                get_secret_value_response["SecretBinary"]
            )
            return decoded_binary_secret


def handler(event, context):
    secret = get_secret()
    # Don't do this in production!!!
    return secret

Enter fullscreen mode Exit fullscreen mode

cdk deploy this stack and you should have a working example.

Test the Lambda Function

Lambda test results

The Lambda function successfully retrieved the secret value from AWS Secrets Manager!

Tear Down

To delete the secret we created:

aws secretsmanager delete-secret --secret-id secretsExample
Enter fullscreen mode Exit fullscreen mode

Destroy your stack:

cdk destroy
Enter fullscreen mode Exit fullscreen mode

Enjoy!

Discussion (0)