Guide to Using AWS Secrets Manager

Introduction

AWS Secrets Manager is a powerful secrets management service from Amazon Web Services, designed to help you protect sensitive information such as database credentials, API keys, and other authentication tokens throughout their lifecycle.

Instead of “hard-coding” sensitive information directly into your source code or application configuration - which carries significant security risks—you can centrally store them on AWS and retrieve them securely through API calls.

Key Advantages

  • Automatic Password Rotation: This is the most valuable feature. Secrets Manager can automatically rotate passwords (for example, for RDS) on a schedule without manual intervention or application downtime.
  • Maximum Security with KMS: All stored information is encrypted using AWS Key Management Service (KMS), ensuring data remains secure even while at rest.
  • Fine-Grained Access Control: Deep integration with AWS IAM allows precise control over which users or services can access specific secrets.
  • Monitoring and Auditing: Seamless integration with AWS CloudTrail enables you to track access history - who retrieved a secret, when, and from where.
  • Reduced Operational Risk: Eliminates the risk of exposing secrets accidentally when pushing code to public repositories (such as GitHub or GitLab).


How to Use

Continue using AWS CDK and create the file lib/secret-manager-stack.ts:

import * as cdk from "aws-cdk-lib"
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"
import * as lambda from "aws-cdk-lib/aws-lambda"
import * as path from "path"
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"

export class SecretManagerStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const myCustomSecret = new secretsmanager.Secret(this, "MyCustomSecret", {
      secretName: "my-app/custom-password",
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: "user1" }),
        generateStringKey: "password",
      },
    })

    const rotationLambda = new NodejsFunction(this, "RotationLambda", {
      entry: path.join(__dirname, "rotation-key.ts"),
      handler: "handler",
      runtime: lambda.Runtime.NODEJS_LATEST,
      bundling: {
        minify: true,
        sourceMap: true,
      },
    })

    myCustomSecret.grantRead(rotationLambda)
    myCustomSecret.grantWrite(rotationLambda)

    myCustomSecret.addRotationSchedule("RotationSchedule", {
      rotationLambda: rotationLambda,
      automaticallyAfter: cdk.Duration.days(30),
    })

    new cdk.CfnOutput(this, "SecretARN", { value: myCustomSecret.secretArn })
  }
}

Explanation:

  • secretsmanager.Secret: used to create a secret, where secretName is the name used to access it.
  • The configuration in generateSecretString will create a JSON object like:

{
    "password": "aeaa14c99647e712028df46ec978fd41",
    "username": "user1"
}

  • The password is randomly generated with a default length of 32 characters; you can customize the length and whether to allow special characters.
  • A Lambda function is created to automatically rotate the password. This is a general approach when you want full control over password rotation. For database passwords, Secrets Manager provides ready-made Lambda templates for simpler rotation.
  • addRotationSchedule: configures password rotation; the minimum interval is 1 hour.


Next, create the file rotation-key.ts as the Lambda function:

import {
  DescribeSecretCommand,
  GetSecretValueCommand,
  PutSecretValueCommand,
  SecretsManagerClient,
  UpdateSecretVersionStageCommand,
} from "@aws-sdk/client-secrets-manager"
import { SecretsManagerRotationEvent } from "aws-lambda"
import * as crypto from "crypto"

const client = new SecretsManagerClient({})

export const handler = async (
  event: SecretsManagerRotationEvent,
): Promise<void> => {
  const { Step, SecretId, ClientRequestToken } = event
  const metadata = await client.send(new DescribeSecretCommand({ SecretId }))
  if (!metadata.RotationEnabled) {
    throw new Error(`Rotation is not enabled for ${SecretId}`)
  }

  switch (Step) {
    case "createSecret":
      await createSecret(SecretId, ClientRequestToken)
      break
    case "setSecret":
      await setSecret(SecretId, ClientRequestToken)
      break
    case "testSecret":
      await testSecret(SecretId, ClientRequestToken)
      break
    case "finishSecret":
      await finishSecret(SecretId, ClientRequestToken)
      break
    default:
      throw new Error("Invalid rotation step")
  }
}

async function createSecret(secretId: string, token: string) {
  const currentSecret = await client.send(
    new GetSecretValueCommand({
      SecretId: secretId,
      VersionStage: "AWSCURRENT",
    }),
  )
  const data = JSON.parse(currentSecret.SecretString || "{}")

  try {
    await client.send(
      new GetSecretValueCommand({ SecretId: secretId, VersionId: token }),
    )
    console.log("createSecret: Version already exists.")
  } catch (e) {
    data.password = crypto.randomBytes(16).toString("hex")
    await client.send(
      new PutSecretValueCommand({
        SecretId: secretId,
        ClientRequestToken: token,
        SecretString: JSON.stringify(data),
        VersionStages: ["AWSPENDING"],
      }),
    )
    console.log("createSecret: New AWSPENDING version created.")
  }
}

async function setSecret(secretId: string, token: string) {
  const pendingSecret = await client.send(
    new GetSecretValueCommand({ SecretId: secretId, VersionId: token }),
  )
  const { password } = JSON.parse(pendingSecret.SecretString || "{}")
  console.log("setSecret: Target service updated with new password.")
}

async function testSecret(secretId: string, token: string) {
  const pendingSecret = await client.send(
    new GetSecretValueCommand({ SecretId: secretId, VersionId: token }),
  )
  const { password } = JSON.parse(pendingSecret.SecretString || "{}")
  console.log("testSecret: New password verified successfully.")
}

async function finishSecret(secretId: string, token: string) {
  const metadata = await client.send(
    new DescribeSecretCommand({ SecretId: secretId }),
  )
  let currentVersionId: string | undefined

  for (const versionId in metadata.VersionIdsToStages) {
    if (metadata.VersionIdsToStages[versionId].includes("AWSCURRENT")) {
      currentVersionId = versionId
      break
    }
  }

  if (currentVersionId === token) return

  await client.send(
    new UpdateSecretVersionStageCommand({
      SecretId: secretId,
      VersionStage: "AWSCURRENT",
      MoveToVersionId: token,
      RemoveFromVersionId: currentVersionId,
    }),
  )
  console.log("finishSecret: Rotation complete. AWSCURRENT updated.")
}

  • Password rotation consists of four stages:
    • createSecret: Generate a new password and assign it the AWSPENDING version stage.
    • setSecret: Synchronize the new password with the target service (for example, call another service’s API to change the password).
    • testSecret: Verify that the new password actually works.
    • finishSecret: Promote the new password version to AWSCURRENT.
  • In this example, setSecret and testSecret are simulated and do not include real processing.


After deploying the stack, a secret will be created.


To use the secret in NestJS, first create the file secret-manager.service.ts:

import {
  GetSecretValueCommand,
  SecretsManagerClient,
} from '@aws-sdk/client-secrets-manager'
import {Injectable} from '@nestjs/common'
import {ConfigService} from '@nestjs/config'

@Injectable()
export class SecretManagerService {
  private client: SecretsManagerClient
  private cachedSecret: any = null
  private lastFetched: number = 0
  private readonly TTL = 1000 * 60 * 5 // 5 minutes

  constructor(private configService: ConfigService) {
    const region = this.configService.get<string>('REGION') || ''
    this.client = new SecretsManagerClient({region})
  }

  async getDatabaseCredentials() {
    const now = Date.now()
    if (this.cachedSecret && now - this.lastFetched < this.TTL) {
      return this.cachedSecret
    }

    try {
      const command = new GetSecretValueCommand({
        SecretId: 'my-app/custom-password',
      })
      const response = await this.client.send(command)
      this.cachedSecret = JSON.parse(response?.SecretString || '')
      this.lastFetched = now
      return this.cachedSecret
    } catch (error) {
      console.error('Error fetching secret', error)
      throw error
    }
  }
}

Just like parameter storage, every call to AWS to retrieve values counts toward request costs ($0.40 / secret / month, $0.05 / 10,000 requests). Therefore, this example caches the secret for 5 minutes before fetching the latest value from AWS again. You can adjust the duration as needed.


Create the file secret-manager.controller.ts:

import {Controller, Get} from '@nestjs/common'
import {SecretManagerService} from 'src/service/secret-manager.service'

@Controller('secret-manager')
export class SecretManagerontroller {
  constructor(private readonly secretManagerService: SecretManagerService) {}

  @Get('info')
  getInfo() {
    return this.secretManagerService.getDatabaseCredentials()
  }
}

This is only a demo example, so an API is created to return secret information. In real applications, these are sensitive details and should not be exposed publicly like this.


Add the service and controller to app.module.ts:

import {Module} from '@nestjs/common'
import {ConfigModule} from '@nestjs/config'
import {AppController} from './app.controller'
import {AppService} from './app.service'
import {SecretManagerService} from './service/secret-manager.service'
import {SecretManagerontroller} from './controller/service-manager.controller'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
  ],
  controllers: [
    AppController,
    SecretManagerontroller,
  ],
  providers: [
    AppService,
    SecretManagerService,
  ],
})
export class AppModule {}


Run the application to see the result.


We can use CloudTrail to check the event history and see which actions have been performed on our secrets.


Happy coding!

See more articles here.

Comments

Popular posts from this blog

All practice series

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Kubernetes Deployment for Zero Downtime

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

NodeJS Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine