AWS Secrets Manager & NodeJS AWS Lambda & Terraform

main

Let's use AWS Secrets Manager with AWS Lambda to keep the secrets safe.

To manage infrastructure, I use Terraform, but you can do the same through AWS CLI or web interface. One caveat is that I don't assign the secrets in Terraform because we don't want secrets in Terraform's state files. Instead, I do that through the AWS web interface. Here I create variables. Then I make a policy to allow Lambda read those variables and finally attach that policy to Lambda's IAM.

resource "aws_secretsmanager_secret" "auth_secret" {
  name = "twitter_client_secret"
}

resource "aws_secretsmanager_secret" "google_client_secret" {
  name = "storybot_mnemonic_key_staging"
}

resource "aws_iam_policy" "secrets" {
  name = "tf-${var.name}-secrets"
  path = "/"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "${aws_secretsmanager_secret.auth_secret.arn}",
      "Effect": "Allow"
    },
    {
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "${aws_secretsmanager_secret.google_client_secret.arn}",
      "Effect": "Allow"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "secrets" {
  role       = module.api.lambda_iam_role_name
  policy_arn = aws_iam_policy.secrets.arn
}

To make the Lambda know the names of those secrets, we can pass them as environment variables to the lambda.

  environment {
    variables = {
      GOOGLE_CLIENT_SECRET_ASM_NAME = aws_secretsmanager_secret.google_client_secret.name
      AUTH_SECRET_ASM_NAME          = aws_secretsmanager_secret.auth_secret.name
    }
  }

To assess those env variables with the names of secrets, I use the assertEnvVar function that will throw an error if the variable is missing.

type VariableName = "GOOGLE_CLIENT_SECRET_ASM_NAME" | "AUTH_SECRET_ASM_NAME"

export const assertEnvVar = (name: VariableName): string => {
  const value = process.env[name]
  if (!value) {
    throw new Error(`Missing ${name} environment variable`)
  }

  return value
}

To get the secrets themselves, I have the assertSecret function that receives an argument of SecretName type, takes the variable name through the assertEnvVar function, and uses an instance of SecretsManager to get the value.

import { SecretsManager } from "aws-sdk"
import { memoize } from "lodash"
import { assertEnvVar } from "./assertEnvVar"

type SecretName = "GOOGLE_CLIENT_SECRET" | "AUTH_SECRET"

const secretsManagerClient = new SecretsManager()

export const assertSecret = memoize(
  async (name: SecretName): Promise<string> => {
    const secretAsmName = assertEnvVar(`${name}_ASM_NAME`)

    const { SecretString: value } = await secretsManagerClient
      .getSecretValue({ SecretId: secretAsmName })
      .promise()

    if (!value) {
      throw new Error(`Missing ${secretAsmName} secret`)
    }
    return value
  }
)