AWS Secrets Manager: Secret Rotation, Lambda Integration, Cross-Account Access, and Terraform
Manage secrets production-ready with AWS Secrets Manager: automatic rotation for RDS and API keys, Lambda integration with caching, cross-account access with resource policies, and Terraform IaC.
Hardcoded secrets in environment variables are a security liability: they're visible in CI logs, committed to .env files accidentally, and can't be rotated without redeployment. AWS Secrets Manager stores secrets encrypted, rotates them automatically on a schedule, and integrates with Lambda, ECS, and EKS via IAM roles — no code changes needed for most rotation scenarios.
This post covers practical Secrets Manager usage: creating and organizing secrets, automatic rotation for RDS credentials and API keys, Lambda integration with in-process caching, cross-account secret sharing, and Terraform IaC.
1. Secret Organization and Naming
/prod/app/database → RDS credentials (JSON)
/prod/app/stripe → Stripe API keys (JSON)
/prod/app/openai → OpenAI API key (string)
/prod/app/encryption-key → AES-256 key for field encryption
/prod/integrations/twilio → Twilio credentials (JSON)
/staging/app/database → Staging RDS credentials
# infrastructure/secrets/main.tf
# Database credentials secret
resource "aws_secretsmanager_secret" "db_credentials" {
name = "/${var.environment}/app/database"
description = "RDS PostgreSQL master credentials"
# Rotate every 30 days automatically
rotation_rules {
automatically_after_days = 30
}
# Prevent accidental deletion
recovery_window_in_days = 7 # 0 for immediate delete (dev only)
tags = {
Environment = var.environment
ManagedBy = "terraform"
Rotation = "automatic"
}
}
# Initial secret value (Terraform sets this once; rotation handles updates)
resource "aws_secretsmanager_secret_version" "db_initial" {
secret_id = aws_secretsmanager_secret.db_credentials.id
secret_string = jsonencode({
username = var.db_username
password = random_password.db_password.result
host = aws_db_instance.main.address
port = 5432
dbname = var.db_name
engine = "postgres"
})
# Don't overwrite if secret already has a value (managed by rotation after first apply)
lifecycle {
ignore_changes = [secret_string]
}
}
# Simple string secret (API key)
resource "aws_secretsmanager_secret" "openai_key" {
name = "/${var.environment}/app/openai"
description = "OpenAI API key"
}
resource "aws_secretsmanager_secret_version" "openai_key" {
secret_id = aws_secretsmanager_secret.openai_key.id
secret_string = var.openai_api_key # Set via TF_VAR_openai_api_key env var
}
2. IAM Policies for Secret Access
# Grant ECS task access to specific secrets (least privilege)
resource "aws_iam_role_policy" "app_secrets" {
name = "${var.project}-app-secrets"
role = aws_iam_role.ecs_task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
]
Resource = [
aws_secretsmanager_secret.db_credentials.arn,
aws_secretsmanager_secret.openai_key.arn,
aws_secretsmanager_secret.stripe.arn,
]
},
{
Effect = "Allow"
Action = ["kms:Decrypt", "kms:GenerateDataKey"]
Resource = aws_kms_key.secrets.arn
}
]
})
}
# KMS key for secret encryption (don't use AWS managed key for prod)
resource "aws_kms_key" "secrets" {
description = "${var.project} secrets encryption key"
deletion_window_in_days = 30
enable_key_rotation = true
tags = { Environment = var.environment }
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
3. Node.js Integration with Caching
Fetching a secret on every request adds 10–50ms of latency and costs $0.05 per 10,000 API calls. Cache secrets in memory with a TTL shorter than the rotation window.
// src/lib/secrets/client.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
interface CacheEntry {
value: string;
expiresAt: number;
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes (rotation window is 30 days)
export async function getSecret(secretId: string): Promise<string> {
const cached = cache.get(secretId);
if (cached && Date.now() < cached.expiresAt) {
return cached.value;
}
const command = new GetSecretValueCommand({ SecretId: secretId });
const response = await client.send(command);
const value = response.SecretString ?? '';
cache.set(secretId, { value, expiresAt: Date.now() + CACHE_TTL_MS });
return value;
}
export async function getSecretJson<T = Record<string, string>>(
secretId: string
): Promise<T> {
const raw = await getSecret(secretId);
return JSON.parse(raw) as T;
}
// Invalidate cache on rotation (called via EventBridge or manual trigger)
export function invalidateSecret(secretId: string): void {
cache.delete(secretId);
}
// Pre-fetch secrets at startup to avoid cold-start latency on first request
export async function warmSecretCache(): Promise<void> {
const secretIds = [
process.env.DB_SECRET_ARN!,
process.env.STRIPE_SECRET_ARN!,
process.env.OPENAI_SECRET_ARN!,
];
await Promise.all(secretIds.map(getSecret));
console.log(`Warmed ${secretIds.length} secrets`);
}
// src/lib/db/index.ts — dynamic database URL from Secrets Manager
import { PrismaClient } from '@prisma/client';
import { getSecretJson } from '../secrets/client';
let prisma: PrismaClient | undefined;
interface DBCredentials {
username: string;
password: string;
host: string;
port: number;
dbname: string;
}
export async function getDb(): Promise<PrismaClient> {
if (prisma) return prisma;
const creds = await getSecretJson<DBCredentials>(process.env.DB_SECRET_ARN!);
prisma = new PrismaClient({
datasourceUrl: `postgresql://${creds.username}:${encodeURIComponent(creds.password)}@${creds.host}:${creds.port}/${creds.dbname}`,
});
return prisma;
}
4. Automatic Rotation for RDS
AWS provides a managed rotation Lambda for RDS — you just enable it.
# Enable automatic rotation using AWS-managed rotation function
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db_credentials.id
rotation_lambda_arn = "arn:aws:lambda:${var.aws_region}:${data.aws_caller_identity.current.account_id}:function:SecretsManagerRDSPostgreSQLRotationSingleUser"
rotation_rules {
automatically_after_days = 30
# Or schedule-based (Secrets Manager 2024+):
# schedule_expression = "rate(30 days)"
}
}
# Grant rotation Lambda access to the secret and the database VPC
resource "aws_lambda_permission" "rotation" {
statement_id = "AllowSecretsManagerRotation"
action = "lambda:InvokeFunction"
function_name = "SecretsManagerRDSPostgreSQLRotationSingleUser"
principal = "secretsmanager.amazonaws.com"
source_arn = aws_secretsmanager_secret.db_credentials.arn
}
Custom Rotation Lambda (for API keys)
// src/functions/secret-rotation/handler.ts
// Called by Secrets Manager at each rotation step
interface RotationEvent {
SecretId: string;
ClientRequestToken: string;
Step: 'createSecret' | 'setSecret' | 'testSecret' | 'finishSecret';
}
export const handler = async (event: RotationEvent): Promise<void> => {
const { SecretsManagerClient, GetSecretValueCommand, PutSecretValueCommand } =
await import('@aws-sdk/client-secrets-manager');
const client = new SecretsManagerClient({});
switch (event.Step) {
case 'createSecret': {
// Generate new API key
const newApiKey = await generateNewApiKey(); // Call your API provider
await client.send(new PutSecretValueCommand({
SecretId: event.SecretId,
ClientRequestToken: event.ClientRequestToken,
SecretString: JSON.stringify({ apiKey: newApiKey }),
VersionStages: ['AWSPENDING'],
}));
break;
}
case 'setSecret': {
// Activate the new key at the provider
const pending = await client.send(new GetSecretValueCommand({
SecretId: event.SecretId,
VersionStage: 'AWSPENDING',
}));
const { apiKey } = JSON.parse(pending.SecretString!);
await activateApiKey(apiKey); // Activate at your API provider
break;
}
case 'testSecret': {
// Verify the new key works
const pending = await client.send(new GetSecretValueCommand({
SecretId: event.SecretId,
VersionStage: 'AWSPENDING',
}));
const { apiKey } = JSON.parse(pending.SecretString!);
const ok = await testApiKey(apiKey);
if (!ok) throw new Error('New API key test failed');
break;
}
case 'finishSecret': {
// Promote AWSPENDING to AWSCURRENT (Secrets Manager handles this)
// Optionally: revoke old key
break;
}
}
};
async function generateNewApiKey(): Promise<string> { /* ... */ return ''; }
async function activateApiKey(key: string): Promise<void> { /* ... */ }
async function testApiKey(key: string): Promise<boolean> { return true; }
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
5. Cross-Account Secret Sharing
# Account A (secret owner): share with Account B
resource "aws_secretsmanager_secret_policy" "cross_account" {
secret_arn = aws_secretsmanager_secret.shared_api_key.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::ACCOUNT_B_ID:role/app-role" }
Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"]
Resource = "*"
}
]
})
}
Cost Reference
| Usage | Monthly cost | Notes |
|---|---|---|
| Secret storage | $0.40/secret/month | First 30 days free per secret |
| API calls | $0.05/10,000 calls | With caching: ~$0.01/month per service |
| Rotation | $0.05/rotation | Uses Lambda invocations |
| KMS encryption | $1.00/key/month + $0.03/10K calls | Use customer-managed key in prod |
See Also
- AWS Lambda Layers: Shared Dependencies and Custom Runtimes
- AWS Step Functions: State Machines and Lambda Orchestration
- Kubernetes Ingress NGINX: TLS, Rate Limiting, and Canary Deployments
- Terraform Modules: Reusable Infrastructure and Remote State
- SaaS Audit Logging: Immutable Trails and SOC2 Compliance
Working With Viprasol
Still storing production database passwords in environment variables or .env files? We migrate your secret management to AWS Secrets Manager with automatic rotation, IAM-scoped access, in-process caching, and full Terraform IaC — so credentials rotate automatically and never appear in logs or CI systems.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.