AWS Secrets Manager¶
1. What is Secrets Manager?¶
AWS Secrets Manager is a fully managed secrets storage and rotation service — it stores sensitive values (database passwords, API keys, OAuth tokens), injects them into applications at runtime, and automatically rotates them on a schedule without any application downtime.
Without Secrets Manager:
DB password hardcoded in app code → committed to Git → visible to all devs
Rotation: manual → update .env → redeploy → downtime → error-prone
With Secrets Manager:
App calls GetSecretValue at runtime → password never in code or env vars
Rotation: automatic → new password generated → DB updated → secret updated
App retries failed DB connection → gets latest secret → reconnects ✅
Audit: every GetSecretValue call logged in CloudTrail
2. Secret Types and Storage¶
Any key-value JSON or plain string:
Database credentials:
{
"username": "admin",
"password": "s3cr3tP@ss!",
"engine": "postgres",
"host": "prod-db.xyz.rds.amazonaws.com",
"port": 5432,
"dbname": "myapp"
}
API keys:
{
"stripe_secret_key": "sk_live_abc123...",
"stripe_webhook_secret": "whsec_xyz789..."
}
OAuth tokens:
{ "access_token": "...", "refresh_token": "..." }
Plain string: "my-api-key-value-here"
Encryption:
Default: encrypted with aws/secretsmanager (AWS managed KMS key) — free
Custom: specify your own CMK for additional control and audit
→ Secret value never stored in plaintext
Size limit: 65,536 bytes per secret value
3. Versioning ⭐¶
Secrets Manager maintains multiple versions of a secret simultaneously:
Version stages (labels):
AWSCURRENT → currently active version (GetSecretValue returns this)
AWSPENDING → newly created version during rotation (not yet validated)
AWSPREVIOUS → previous AWSCURRENT (kept as fallback after rotation)
Version lifecycle during rotation:
1. createSecret phase:
→ New version created with AWSPENDING label
→ AWSCURRENT still serving old credentials → zero downtime
2. setSecret phase:
→ New credentials applied to target (DB password changed)
→ Both old and new credentials work at this moment
3. testSecret phase:
→ Test that AWSPENDING credentials actually work
4. finishSecret phase:
→ AWSPENDING → promoted to AWSCURRENT
→ Old AWSCURRENT → becomes AWSPREVIOUS
→ Applications now get new credentials on next GetSecretValue call
4. Automatic Rotation ⭐¶
Secrets Manager rotates secrets using a Lambda function you define or use from AWS-managed templates:
Rotation schedule options:
Every N days: rotate every 30 days
Cron expression: "cron(0 0 1 * ? *)" → 1st of every month at midnight
Immediate + scheduled: rotate now AND on schedule
Built-in managed rotation (no Lambda code needed): [aws.amazon](https://aws.amazon.com/blogs/security/rotate-amazon-rds-database-credentials-automatically-with-aws-secrets-manager/)
Amazon RDS (MySQL, PostgreSQL, Aurora, Oracle, SQL Server)
Amazon Redshift
Amazon DocumentDB
→ AWS provides pre-built Lambda functions for these
→ Select: "Managed rotation" → AWS creates Lambda automatically
Custom rotation (Lambda): [docs.aws.amazon](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda.html)
For: any other service (API keys, OAuth tokens, custom databases)
You write Lambda with four handler phases (see below)
Lambda rotation must be in the SAME VPC as the database
(or use Secrets Manager VPC endpoint if Lambda is in private subnet)
Four-Phase Rotation Lambda¶
def lambda_handler(event, context):
step = event['Step']
if step == 'createSecret':
# Generate new credentials (new random password)
# Store as AWSPENDING version of the secret
new_password = generate_password()
client.put_secret_value(
SecretId=arn,
ClientRequestToken=token,
SecretString=json.dumps({...new_credentials...}),
VersionStages=['AWSPENDING']
)
elif step == 'setSecret':
# Apply new credentials to the actual service
# (ALTER USER in MySQL, update API provider, etc.)
apply_to_database(new_credentials)
elif step == 'testSecret':
# Verify new credentials actually work
# Raise exception if test fails → rotation aborted, AWSPENDING discarded
test_database_connection(new_credentials)
elif step == 'finishSecret':
# Mark AWSPENDING as AWSCURRENT, old current becomes AWSPREVIOUS
client.update_secret_version_stage(
SecretId=arn,
VersionStage='AWSCURRENT',
MoveToVersionId=token,
RemoveFromVersionId=current_version
)
RDS Read Replica Rotation¶
Challenge: rotating primary DB credentials → read replicas also need update
Step 1: Lambda rotates primary instance password
Step 2: Lambda applies same new password to all read replicas
Step 3: Test connectivity on primary AND all replicas
Step 4: Promote new secret version to AWSCURRENT
Both primary + replica credential updates happen atomically in one rotation cycle [aws.amazon](https://aws.amazon.com/blogs/database/automate-amazon-rds-credential-rotation-with-aws-secrets-manager-for-primary-instances-with-read-replicas/)
5. Application Integration ⭐¶
Retrieve Secret in Application Code¶
import boto3, json
def get_db_credentials():
client = boto3.client('secretsmanager', region_name='us-east-1')
response = client.get_secret_value(
SecretId='production/database/master'
)
return json.loads(response['SecretString'])
# Usage
creds = get_db_credentials()
conn = psycopg2.connect(
host=creds['host'],
port=creds['port'],
dbname=creds['dbname'],
user=creds['username'],
password=creds['password']
)
Handle Rotation Gracefully¶
# Robust pattern — retry on auth failure (rotation may have just happened)
import time
def get_connection(retries=2):
for attempt in range(retries):
try:
creds = get_secret() # always get latest from Secrets Manager
return connect(creds)
except AuthenticationError:
if attempt < retries - 1:
time.sleep(1) # brief wait then retry with fresh secret
continue
raise
# Why: during rotation, AWSPENDING becomes AWSCURRENT
# Your cached credentials → connection fails → retry → get new secret → success
ECS / Lambda Integration¶
ECS Task Definition:
secrets:
- name: DB_PASSWORD
valueFrom: "arn:aws:secretsmanager:us-east-1:123:secret:prod/db/password"
→ Injected as environment variable at container startup
→ Container gets fresh value on each new task launch
Lambda:
environment:
Variables:
SECRET_ARN: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:prod/db"
In code: boto3.client('secretsmanager').get_secret_value(SecretId=os.environ['SECRET_ARN'])
Better pattern for Lambda (avoid cold start latency):
Cache secret in module scope (outside handler)
Refresh if secret is > 5 minutes old
→ Avoids Secrets Manager API call on every invocation
6. Cross-Account Access¶
Account A owns the secret: production/database/master
Account B (Lambda in B) needs to read it
Steps:
1. Secret resource policy in Account A:
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT-B:role/MyLambdaRole" },
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}
2. KMS key policy in Account A (if custom CMK used):
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::ACCOUNT-B:role/MyLambdaRole" },
"Action": ["kms:Decrypt", "kms:DescribeKey"],
"Resource": "*"
}
3. IAM policy in Account B for MyLambdaRole:
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:ACCOUNT-A:secret:production/database/master"
}
All three required for cross-account secrets access
7. Secrets Manager vs SSM Parameter Store ⭐¶
| Feature | Secrets Manager | SSM Parameter Store |
|---|---|---|
| Primary purpose | Secrets lifecycle management | Configuration + secrets storage |
| Automatic rotation | ✅ Built-in (Lambda-based) | ❌ Manual or custom Lambda |
| Cost | $0.40/secret/month + $0.05/10K API calls | Free (Standard) / $0.05/advanced param/month |
| Versioning | Automatic (AWSCURRENT/PENDING/PREVIOUS) | Up to 100 versions, manual |
| Cross-account | ✅ Resource policy | Limited (Advanced tier) |
| Max value size | 65,536 bytes | 4 KB (Standard) / 8 KB (Advanced) |
| Hierarchical naming | Flat (use / in name as convention) | Native hierarchies: /app/prod/db-pass |
| GetParameters by path | ❌ | ✅ GetParametersByPath |
| Native RDS integration | ✅ Managed rotation templates | ❌ |
| Use case | Database passwords, API keys needing rotation | App config, feature flags, non-rotating secrets |
Rule of thumb:
Needs rotation? → Secrets Manager
Static config values? → SSM Parameter Store (free, simpler)
Hierarchical config? → SSM Parameter Store
Database credentials? → Secrets Manager always
8. Common Mistakes¶
| ❌ Wrong | ✅ Correct |
|---|---|
| GetSecretValue in a Lambda on every request | Cache the secret (module scope) + refresh after TTL — avoid per-invocation API calls |
| Rotation causes downtime | Rotation is zero-downtime — AWSPENDING created before AWSCURRENT swapped |
| Lambda rotation function can be in any VPC | Rotation Lambda must be in the same VPC as the target DB (or use Secrets Manager VPC endpoint) |
| Old secret versions deleted after rotation | AWSPREVIOUS version kept — applications can still decrypt with it during transition |
| Secrets Manager encrypts with its own key | Default: uses aws/secretsmanager (AWS managed KMS key) — or specify your CMK |
| SSM Parameter Store can auto-rotate | Parameter Store has no built-in rotation — must build custom Lambda rotation |
| Resource policy alone allows cross-account | Requires: secret resource policy + KMS key policy + IAM policy in target account — all three |
9. Interview Questions Checklist¶
- How does automatic rotation work? Name the four Lambda phases
- What are AWSCURRENT, AWSPENDING, AWSPREVIOUS?
- Why is AWSPREVIOUS kept after rotation? (apps may still use old version briefly)
- How do you handle rotation gracefully in your application?
- Secrets Manager vs SSM Parameter Store — when to use each?
- What three things are needed for cross-account secrets access?
- Where must the rotation Lambda be deployed? (same VPC as target DB)
- How to rotate RDS with read replicas?