Skip to main content

Managing Secrets with AWS Secrets Manager

AWS Secrets Manager stores sensitive values — database passwords, API keys, third-party credentials — encrypted at rest and lets your application retrieve them at runtime. Unlike environment variables in a .env file, secrets stored in Secrets Manager are:

  • Encrypted using AWS KMS
  • Access-controlled via IAM
  • Auditable (every access is logged in CloudTrail)
  • Rotatable automatically (for RDS passwords)

When to use Secrets Manager vs .env

ScenarioUse
Local development.env file
Staging / Production on AWS (EC2, ECS, Lambda)Secrets Manager
Config values that aren't sensitive (region, bucket name)Environment variables
Passwords, API keys, tokensSecrets Manager

Create a Secret

  1. Go to AWS Console → Secrets Manager → Store a new secret
  2. Choose Other type of secret
  3. Add your key-value pairs (e.g., DB_PASSWORD, STRIPE_SECRET_KEY)
  4. Name the secret (e.g., my-app/production)
  5. Leave rotation disabled for now
  6. Click Store

Note the Secret name or ARN — you'll use it in your app.

Install

npm install @aws-sdk/client-secrets-manager

Retrieve a Secret

config/secrets.js
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

const client = new SecretsManagerClient({ region: process.env.AWS_REGION });

export async function getSecret(secretName) {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);

if (response.SecretString) {
return JSON.parse(response.SecretString);
}

// Binary secret (rare)
return JSON.parse(Buffer.from(response.SecretBinary, "base64").toString());
}

Load Secrets at App Startup

The correct pattern is to load secrets once when the app starts, not on every request. Calling Secrets Manager on every request is slow and expensive.

index.js
import express from "express";
import "dotenv/config";
import mongoose from "mongoose";
import { getSecret } from "./config/secrets.js";

async function start() {
// Load secrets from Secrets Manager before starting the server
const secrets = await getSecret("my-app/production");

// Inject into process.env so the rest of your app can use them normally
process.env.DB_PASSWORD = secrets.DB_PASSWORD;
process.env.STRIPE_SECRET_KEY = secrets.STRIPE_SECRET_KEY;
process.env.JWT_SECRET = secrets.JWT_SECRET;

// Now connect to the database using the retrieved password
await mongoose.connect(
`mongodb+srv://user:${secrets.DB_PASSWORD}@cluster.mongodb.net/mydb`
);

const app = express();
app.use(express.json());

// ... routes

app.listen(3000, () => console.log("Server running"));
}

start().catch((err) => {
console.error("Failed to start:", err);
process.exit(1);
});
On AWS compute (EC2, ECS, Lambda)

When running on AWS, remove the credentials from the Secrets Manager client. The IAM role attached to your compute resource grants access automatically — no access keys needed.

In AWS Lambda, the function can be invoked thousands of times. You don't want to call Secrets Manager on every cold start. Cache the secret in the module scope — it persists across warm invocations:

config/secrets.js
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
const cache = new Map();

export async function getSecret(secretName) {
if (cache.has(secretName)) {
return cache.get(secretName);
}

const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
const secret = JSON.parse(response.SecretString);

cache.set(secretName, secret);
return secret;
}

For even more sophisticated caching (TTL-based refresh), use the official @aws-sdk/client-secrets-manager caching layer.

Organizing Secrets

Use a naming convention that separates environments and services:

my-app/development/database
my-app/development/third-party
my-app/production/database
my-app/production/third-party

This lets you scope IAM policies tightly:

IAM policy — production only
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789:secret:my-app/production/*"
}

Rotating Secrets Automatically

For RDS database passwords, Secrets Manager can rotate the password automatically on a schedule:

  1. Go to your secret → Rotation → Edit rotation
  2. Enable rotation, set a schedule (e.g., every 30 days)
  3. Choose the Lambda rotation function AWS provides for RDS

Your app doesn't need any changes — it fetches the current secret value at startup, so it always gets the latest rotated password.

Key Takeaways

  • Fetch secrets once at startup, not per request
  • On AWS compute, rely on IAM roles instead of hardcoded credentials in the Secrets Manager client
  • Use a naming convention like app/environment/service for easy IAM scoping
  • Use automatic rotation for database passwords — Secrets Manager + RDS makes this zero-effort
  • Secrets Manager has a small cost per secret per month — for non-sensitive config values, use environment variables instead