Back to Blog

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.

Viprasol Tech Team
December 6, 2026
13 min read

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

UsageMonthly costNotes
Secret storage$0.40/secret/monthFirst 30 days free per secret
API calls$0.05/10,000 callsWith caching: ~$0.01/month per service
Rotation$0.05/rotationUses Lambda invocations
KMS encryption$1.00/key/month + $0.03/10K callsUse customer-managed key in prod

See Also


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.

Talk to our team → | Explore our cloud solutions →

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need DevOps & Cloud Expertise?

Scale your infrastructure with confidence. AWS, GCP, Azure certified team.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

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.