Secrets Manager & Parameter Store
A comprehensive deep dive into AWS Secrets Manager and SSM Parameter Store — secret types, automatic rotation, versioning, hierarchies, SDK patterns, Lambda integration, cross-account access, and DVA-C02 exam essentials.
Secrets Manager vs Parameter Store — Mental Model
Both services store sensitive configuration, but they solve different problems.
Core mental model: Secrets Manager is a secrets lifecycle manager — it stores, versions, rotates, and audits credentials automatically. Parameter Store is a configuration store that also handles secrets as a side-effect — free, hierarchical, and great for non-rotating config values.
Part 1 — AWS Secrets Manager
Core Concepts
| Feature | Detail |
|---|---|
| Storage | Encrypted JSON blob, up to 64 KB |
| Encryption | KMS (default: aws/secretsmanager, or CMK) |
| Versioning | AWSCURRENT, AWSPENDING, AWSPREVIOUS labels |
| Auto-rotation | Built-in for RDS, Redshift, DocumentDB, Aurora |
| Custom rotation | Lambda function for any other secret type |
| Cost | $0.40/secret/month + $0.05 per 10,000 API calls |
| Cross-account | Resource-based policy on the secret |
| Replication | Replicate secrets to multiple regions |
Creating and Retrieving Secrets (SDK)
1import {
2 SecretsManagerClient,
3 CreateSecretCommand,
4 GetSecretValueCommand,
5 PutSecretValueCommand,
6 RotateSecretCommand,
7 DeleteSecretCommand,
8} from '@aws-sdk/client-secrets-manager';
9
10const sm = new SecretsManagerClient({ region: 'us-east-1' });
11
12// ── CREATE ────────────────────────────────────────────────────────────────────
13await sm.send(new CreateSecretCommand({
14 Name: '/myapp/prod/db-credentials',
15 Description: 'Production RDS credentials',
16 SecretString: JSON.stringify({
17 username: 'admin',
18 password: 'S3cur3P@ss!',
19 host: 'mydb.cluster.us-east-1.rds.amazonaws.com',
20 port: 5432,
21 dbname: 'appdb',
22 }),
23 KmsKeyId: 'alias/my-secrets-key', // omit to use aws/secretsmanager
24 Tags: [{ Key: 'Environment', Value: 'prod' }],
25}));
26
27// ── GET CURRENT VALUE ─────────────────────────────────────────────────────────
28const { SecretString } = await sm.send(new GetSecretValueCommand({
29 SecretId: '/myapp/prod/db-credentials',
30 // VersionStage defaults to AWSCURRENT
31}));
32const creds = JSON.parse(SecretString);
33console.log(creds.password); // S3cur3P@ss!
34
35// ── GET A SPECIFIC VERSION ────────────────────────────────────────────────────
36const { SecretString: prev } = await sm.send(new GetSecretValueCommand({
37 SecretId: '/myapp/prod/db-credentials',
38 VersionStage: 'AWSPREVIOUS',
39}));Secret Versioning & Rotation Labels
| Version Stage | Meaning |
|---|---|
AWSCURRENT | The active, in-use secret value |
AWSPENDING | New value being rotated in (during rotation window) |
AWSPREVIOUS | Previous value (kept temporarily for in-flight connections) |
| Custom stage | Your own labels for blue/green or versioned deploys |
Automatic Rotation Setup
1# Enable automatic rotation — every 30 days for an RDS secret
2aws secretsmanager rotate-secret --secret-id /myapp/prod/db-credentials --rotation-rules AutomaticallyAfterDays=30 --rotate-immediately # rotate right now as well
3
4# Enable for RDS using the managed rotation function (no Lambda needed)
5aws secretsmanager rotate-secret --secret-id /myapp/prod/db-credentials --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSMySQLRotationSingleUser --rotation-rules AutomaticallyAfterDays=30AWS provides managed rotation Lambda functions for:
- RDS MySQL / PostgreSQL / Oracle / SQL Server (single-user and multi-user)
- Amazon Redshift
- Amazon DocumentDB
- Amazon ElastiCache (Redis)
For anything else (third-party APIs, custom databases), you write a Lambda with four lifecycle hooks:
| Lambda Hook | What it does |
|---|---|
createSecret | Generate new credential value, store as AWSPENDING |
setSecret | Set the new credential in the target service (DB, API) |
testSecret | Verify the AWSPENDING secret actually works |
finishSecret | Move AWSPENDING → AWSCURRENT, old → AWSPREVIOUS |
1// Custom rotation Lambda skeleton
2export async function handler(event) {
3 const { Step, SecretId, ClientRequestToken } = event;
4
5 switch (Step) {
6 case 'createSecret':
7 // Generate new password and call sm.putSecretValue with VersionStages: ['AWSPENDING']
8 break;
9 case 'setSecret':
10 // Retrieve AWSPENDING secret, update the target resource
11 break;
12 case 'testSecret':
13 // Retrieve AWSPENDING secret, attempt to authenticate with target resource
14 // Throw on failure — Secrets Manager will halt rotation
15 break;
16 case 'finishSecret':
17 // Call sm.updateSecretVersionStage to move AWSPENDING → AWSCURRENT
18 break;
19 }
20}Caching Secrets in Lambda (Best Practice)
Calling GetSecretValue on every Lambda invocation adds latency and cost. Use the Secrets Manager caching client:
1import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
2
3const sm = new SecretsManagerClient({});
4
5// Module-level cache — survives warm invocations
6let cachedSecret = null;
7let cacheExpiry = 0;
8const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
9
10async function getSecret(secretId) {
11 const now = Date.now();
12 if (cachedSecret && now < cacheExpiry) return cachedSecret;
13
14 const { SecretString } = await sm.send(
15 new GetSecretValueCommand({ SecretId: secretId })
16 );
17 cachedSecret = JSON.parse(SecretString);
18 cacheExpiry = now + CACHE_TTL_MS;
19 return cachedSecret;
20}
21
22export async function handler() {
23 const creds = await getSecret('/myapp/prod/db-credentials');
24 // use creds.password
25}Cross-Account Secret Access
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Sid": "AllowCrossAccountRead",
6 "Effect": "Allow",
7 "Principal": {
8 "AWS": "arn:aws:iam::987654321098:role/AppRole"
9 },
10 "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
11 "Resource": "*"
12 }
13 ]
14}The KMS key used to encrypt the secret must also grant the cross-account principal kms:Decrypt — otherwise the decrypted value cannot be returned even if the resource policy allows it.
Secret Replication (Multi-Region)
1# Replicate a secret to another region (read replica — auto-synced)
2aws secretsmanager replicate-secret-to-regions --secret-id /myapp/prod/db-credentials --add-replica-regions '[{"Region":"eu-west-1"},{"Region":"ap-southeast-1"}]'Each replica is independently encrypted with a KMS key in that region. Use replicas for low-latency reads and DR scenarios.
Part 2 — SSM Parameter Store
Parameter Types
| Type | Encrypted | Max Size | Cost |
|---|---|---|---|
| String | No | 4 KB (std) / 8 KB (adv) | Free (std) |
| StringList | No | 4 KB / 8 KB | Free (std) |
| SecureString | Yes (KMS) | 4 KB / 8 KB | Free (std), $0.05/param (adv) |
Standard vs Advanced
| Standard | Advanced | |
|---|---|---|
| Max parameter size | 4 KB | 8 KB |
| Parameter policies (TTL, expiry) | No | Yes |
| Max parameters per account | 10,000 | Unlimited |
| Throughput | 40 TPS (default) | 3,000 TPS |
| Cost | Free | $0.05/param/month |
| Cost to retrieve | Free | $0.05 per 10,000 API calls |
Hierarchy & Naming Convention
1/ ← root
2├── myapp/
3│ ├── dev/
4│ │ ├── db-url String
5│ │ ├── db-password SecureString
6│ │ └── feature-flags StringList
7│ └── prod/
8│ ├── db-url
9│ ├── db-password
10│ └── api-key
11└── shared/
12 └── slack-webhook1# Put a standard string
2aws ssm put-parameter --name "/myapp/prod/db-url" --value "postgres://mydb.us-east-1.rds.amazonaws.com:5432/appdb" --type String --overwrite
3
4# Put a SecureString encrypted with a CMK
5aws ssm put-parameter --name "/myapp/prod/db-password" --value "S3cur3P@ss!" --type SecureString --key-id alias/my-param-key --overwrite
6
7# Get a single parameter (auto-decrypt SecureString)
8aws ssm get-parameter --name "/myapp/prod/db-password" --with-decryption
9
10# Get ALL parameters under a path (recursive)
11aws ssm get-parameters-by-path --path "/myapp/prod" --recursive --with-decryptionSDK Usage in Node.js
1import { SSMClient, GetParameterCommand, GetParametersByPathCommand, PutParameterCommand } from '@aws-sdk/client-ssm';
2
3const ssm = new SSMClient({ region: 'us-east-1' });
4
5// ── READ SINGLE ───────────────────────────────────────────────────────────────
6async function getParam(name) {
7 const { Parameter } = await ssm.send(new GetParameterCommand({
8 Name: name,
9 WithDecryption: true, // required for SecureString
10 }));
11 return Parameter.Value;
12}
13
14// ── READ ALL UNDER A PATH ─────────────────────────────────────────────────────
15async function getParamsByPath(path) {
16 const params = {};
17 let nextToken;
18
19 do {
20 const { Parameters, NextToken } = await ssm.send(
21 new GetParametersByPathCommand({
22 Path: path,
23 Recursive: true,
24 WithDecryption: true,
25 NextToken: nextToken,
26 })
27 );
28 for (const p of Parameters) {
29 // /myapp/prod/db-url → 'db-url'
30 const key = p.Name.split('/').pop();
31 params[key] = p.Value;
32 }
33 nextToken = NextToken;
34 } while (nextToken);
35
36 return params; // { 'db-url': '...', 'db-password': '...', 'api-key': '...' }
37}
38
39// ── WRITE ─────────────────────────────────────────────────────────────────────
40await ssm.send(new PutParameterCommand({
41 Name: '/myapp/prod/feature-dark-mode',
42 Value: 'true',
43 Type: 'String',
44 Overwrite: true,
45}));Parameter Policies (Advanced Only)
Parameter policies allow automatic expiry and notifications — no code needed.
1# Advanced parameter with an expiry policy (auto-deletes after date)
2aws ssm put-parameter --name "/myapp/prod/temp-api-key" --value "sk-temp-abc123" --type SecureString --tier Advanced --policies '[
3 {
4 "Type": "Expiration",
5 "Version": "1.0",
6 "Attributes": {
7 "Timestamp": "2025-12-31T00:00:00.000Z"
8 }
9 },
10 {
11 "Type": "ExpirationNotification",
12 "Version": "1.0",
13 "Attributes": {
14 "Before": "10",
15 "Unit": "Days"
16 }
17 },
18 {
19 "Type": "NoChangeNotification",
20 "Version": "1.0",
21 "Attributes": {
22 "After": "30",
23 "Unit": "Days"
24 }
25 }
26 ]'| Policy Type | What it does |
|---|---|
Expiration | Deletes the parameter at a specified timestamp |
ExpirationNotification | Sends EventBridge event N days before expiry |
NoChangeNotification | Sends EventBridge event if parameter not updated in N days |
Loading Config at Lambda Cold Start
1// Load all config once at module init (cold start only)
2const ssm = new SSMClient({});
3
4let config;
5
6async function loadConfig() {
7 if (config) return config;
8 const params = await getParamsByPath('/myapp/prod');
9 config = params;
10 return config;
11}
12
13export async function handler(event) {
14 const { 'db-url': dbUrl, 'db-password': dbPass } = await loadConfig();
15 // connect to DB using dbUrl and dbPass
16}This runs GetParametersByPath once per cold start, then reuses the in-memory config for all warm invocations.
Integration with CloudFormation & CDK
1# CloudFormation — reference a Parameter Store value
2Parameters:
3 DbPassword:
4 Type: 'AWS::SSM::Parameter::Value<String>'
5 Default: /myapp/prod/db-password
6
7Resources:
8 RDSInstance:
9 Type: AWS::RDS::DBInstance
10 Properties:
11 MasterUserPassword: !Ref DbPassword1// CDK — read Parameter Store value at synth time
2import * as ssm from 'aws-cdk-lib/aws-ssm';
3
4const dbUrl = ssm.StringParameter.valueForStringParameter(
5 this, '/myapp/prod/db-url'
6);
7
8// CDK — read Secrets Manager secret at deploy time
9import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
10
11const secret = secretsmanager.Secret.fromSecretNameV2(
12 this, 'DbSecret', '/myapp/prod/db-credentials'
13);
14const dbPassword = secret.secretValueFromJson('password').unsafeUnwrap();ECS & Kubernetes Integration
1// ECS Task Definition — inject secrets as environment variables
2{
3 "containerDefinitions": [{
4 "name": "app",
5 "image": "myapp:latest",
6 "secrets": [
7 {
8 "name": "DB_PASSWORD",
9 "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/prod/db-credentials:password::"
10 },
11 {
12 "name": "API_KEY",
13 "valueFrom": "/myapp/prod/api-key"
14 }
15 ],
16 "environment": [
17 { "name": "NODE_ENV", "value": "production" }
18 ]
19 }]
20}ECS fetches the secrets at task launch and injects them as environment variables. The task execution role must have secretsmanager:GetSecretValue or ssm:GetParameters plus the KMS decrypt permission.
Rotation Architecture (Full Flow)
Comparison: Secrets Manager vs Parameter Store
| Secrets Manager | Parameter Store | |
|---|---|---|
| Cost | $0.40/secret/month | Free (standard), $0.05/param (advanced) |
| Max size | 64 KB | 4 KB (std) / 8 KB (adv) |
| Auto rotation | Built-in Lambda rotation | ❌ Manual (you write Lambda + EventBridge) |
| Versioning labels | AWSCURRENT / AWSPENDING / AWSPREVIOUS | Version numbers (integer) |
| Expiry / TTL | ❌ (delete manually) | ✅ Advanced — Expiration policy |
| Cross-account | Resource policy | ❌ IAM cross-account only via roles |
| Multi-region replication | ✅ Built-in | ❌ |
| CloudFormation dynamic ref | {{resolve:secretsmanager:name:SecretString:key}} | {{resolve:ssm-secure:/path/name}} |
| CloudTrail audit | Every GetSecretValue logged | Every GetParameter logged |
| Hierarchy / path naming | Flat (use / as convention only) | True hierarchical path with GetParametersByPath |
| Best for | DB credentials, API keys, rotating secrets | App config, feature flags, any non-rotating config |
CloudFormation Dynamic References
1# Secrets Manager dynamic reference
2MyFunction:
3 Type: AWS::Lambda::Function
4 Properties:
5 Environment:
6 Variables:
7 DB_PASSWORD: '{{resolve:secretsmanager:/myapp/prod/db-credentials:SecretString:password}}'
8
9# Parameter Store dynamic reference (SecureString)
10 API_KEY: '{{resolve:ssm-secure:/myapp/prod/api-key}}'
11
12# Parameter Store dynamic reference (plain String)
13 DB_HOST: '{{resolve:ssm:/myapp/prod/db-url}}'Security Best Practices
| Practice | Why |
|---|---|
| Never hardcode secrets in code or env files | Leaked via git history, process listings |
| Use Parameter Store for non-rotating config | Cheaper than Secrets Manager for static values |
| Enable rotation immediately for DB credentials | Limits blast radius if credentials are compromised |
| Use dedicated KMS CMK per environment | Separate dev/staging/prod key access |
| Scope IAM to specific secret ARNs | Least privilege — avoid secretsmanager:* on * |
| Enable CloudTrail | Audit every secret access attempt |
| Use VPC endpoint for Secrets Manager | Keeps traffic off the public internet |
| Cache secrets in Lambda | Reduces cost and latency without sacrificing freshness |
DVA-C02 Quick Reference
| Topic | Key Fact |
|---|---|
| Secrets Manager cost | $0.40/secret/month |
| Parameter Store standard cost | Free |
| Parameter Store advanced cost | $0.05/parameter/month |
| Max secret size | 64 KB |
| Max standard parameter size | 4 KB |
| Max advanced parameter size | 8 KB |
| Rotation stages | AWSCURRENT → AWSPENDING → AWSCURRENT (old becomes AWSPREVIOUS) |
| Rotation Lambda hooks (order) | createSecret → setSecret → testSecret → finishSecret |
| Built-in rotation support | RDS, Redshift, DocumentDB, Aurora, ElastiCache |
| Custom rotation | Lambda with all 4 lifecycle hooks |
| SecureString encryption | KMS (CMK or aws/ssm) |
| GetParametersByPath | Retrieve all params under a path prefix recursively |
| Parameter policies require | Advanced tier |
| Cross-account Secrets Manager | Resource-based policy + KMS key policy grants |
| Replicate secret across regions | replicate-secret-to-regions — each region gets own KMS key |
| CloudFormation secret ref syntax | {{resolve:secretsmanager:name:SecretString:jsonKey}} |
| CloudFormation SecureString ref | {{resolve:ssm-secure:/path/param}} |
| ECS secrets injection | secrets in task definition — fetched at task launch |
| Secrets Manager vs Parameter Store | Secrets Manager: rotation + lifecycle; Parameter Store: config + free tier |
Practice Questions2
Q1. A developer stores an RDS database password in AWS Secrets Manager. The application retrieves it at startup. Six months later, the application breaks because the password changed. What Secrets Manager feature caused this?
Select one answer before revealing.
Q2. A developer needs to store API keys, database passwords, and TLS certificates as configuration values accessible to Lambda functions. Some values are sensitive (must be encrypted); others are non-sensitive. Which service combination is recommended?
Select one answer before revealing.