AWS Secrets Manager helps you protect access to your applications, services, and IT resources without the upfront cost and complexity of managing your own secret management infrastructure. This tutorial covers creating secrets, setting up automatic rotation, and accessing secrets from Lambda, EC2, and other AWS services.
This article is part of our comprehensive Cloud Security Tips for 2026 guide covering essential practices for protecting your cloud environment.
Why Use Secrets Manager
- Eliminate hardcoded credentials - No more passwords in source code
- Automatic rotation - Rotate secrets without application changes
- Fine-grained access - IAM policies control who can access secrets
- Audit trail - CloudTrail logs all secret access
- Encryption - All secrets encrypted with AWS KMS
Create a Secret
Using AWS Console
- Open the Secrets Manager Console
- Click Store a new secret
- Choose secret type (database credentials, API key, or other)
- Enter secret values
- Name the secret (e.g., "prod/database/postgres")
- Configure rotation (optional)
- Click Store
Using AWS CLI
# Create a simple key-value secret
aws secretsmanager create-secret \
--name prod/api/stripe \
--description "Stripe API keys for production" \
--secret-string '{"api_key":"sk_live_xxx","webhook_secret":"whsec_xxx"}'
# Create database credentials
aws secretsmanager create-secret \
--name prod/database/postgres \
--description "PostgreSQL production credentials" \
--secret-string '{
"username":"admin",
"password":"MySecurePassword123!",
"engine":"postgres",
"host":"mydb.cluster-xyz.us-east-1.rds.amazonaws.com",
"port":5432,
"dbname":"production"
}'Use Custom KMS Key
# Create with customer-managed KMS key
aws secretsmanager create-secret \
--name prod/database/postgres \
--kms-key-id alias/secrets-key \
--secret-string '{"username":"admin","password":"MySecurePassword123!"}'Retrieve Secrets
AWS CLI
# Get secret value
aws secretsmanager get-secret-value \
--secret-id prod/database/postgres \
--query SecretString \
--output text
# Get specific version
aws secretsmanager get-secret-value \
--secret-id prod/database/postgres \
--version-stage AWSCURRENT
# Parse JSON with jq
aws secretsmanager get-secret-value \
--secret-id prod/database/postgres \
--query SecretString \
--output text | jq -r '.password'Python (boto3)
import boto3
import json
def get_secret(secret_name, region_name="us-east-1"):
client = boto3.client('secretsmanager', region_name=region_name)
response = client.get_secret_value(SecretId=secret_name)
if 'SecretString' in response:
return json.loads(response['SecretString'])
else:
# Binary secret
return response['SecretBinary']
# Usage
db_creds = get_secret('prod/database/postgres')
print(f"Connecting as {db_creds['username']}")Node.js
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
async function getSecret(secretName) {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString);
}
// Usage
const dbCreds = await getSecret('prod/database/postgres');
console.log(`Connecting as ${dbCreds.username}`);Access Secrets from Lambda
Step 1: Create IAM Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/*"
]
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": [
"arn:aws:kms:us-east-1:123456789012:key/*"
],
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
}
}
}
]
}Step 2: Lambda Function with Caching
import boto3
import json
from functools import lru_cache
secrets_client = boto3.client('secretsmanager')
@lru_cache(maxsize=10)
def get_cached_secret(secret_name):
"""Cache secrets to reduce API calls and latency"""
response = secrets_client.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
def lambda_handler(event, context):
# Secret is cached for Lambda container lifetime
db_creds = get_cached_secret('prod/database/postgres')
# Connect to database
connection = connect_to_database(
host=db_creds['host'],
user=db_creds['username'],
password=db_creds['password'],
database=db_creds['dbname']
)
# Process event
return {'statusCode': 200}Using AWS Parameters and Secrets Lambda Extension
AWS provides a Lambda extension for efficient secret caching:
# Add layer ARN to Lambda
arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11
# Access via local HTTP endpoint
import urllib.request
import os
def get_secret_from_extension(secret_name):
headers = {"X-Aws-Parameters-Secrets-Token": os.environ['AWS_SESSION_TOKEN']}
url = f"http://localhost:2773/secretsmanager/get?secretId={secret_name}"
req = urllib.request.Request(url, headers=headers)
response = urllib.request.urlopen(req)
return json.loads(response.read())['SecretString']Access Secrets from EC2
Using Instance Profile (Recommended)
# EC2 instance automatically gets credentials from instance profile
# Python code works the same as Lambda
import boto3
import json
client = boto3.client('secretsmanager', region_name='us-east-1')
response = client.get_secret_value(SecretId='prod/database/postgres')
secret = json.loads(response['SecretString'])Using AWS CLI on EC2
# Export as environment variables
export DB_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id prod/database/postgres \
--query 'SecretString' \
--output text | jq -r '.password')
# Use in application
psql -h $DB_HOST -U admin -d productionSet Up Automatic Rotation
For RDS Databases (Built-in Rotation)
# Enable rotation for RDS secret
aws secretsmanager rotate-secret \
--secret-id prod/database/postgres \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSPostgreSQLRotation \
--rotation-rules AutomaticallyAfterDays=30
# For managed RDS rotation, create through console or:
aws secretsmanager create-secret \
--name prod/database/postgres \
--secret-string '{"username":"admin","password":"initial"}' \
--add-replica-regions '[{"Region":"us-west-2"}]'Custom Rotation Lambda
import boto3
import json
import string
import secrets
def lambda_handler(event, context):
secret_id = event['SecretId']
step = event['Step']
token = event['ClientRequestToken']
sm_client = boto3.client('secretsmanager')
if step == 'createSecret':
# Generate new password
new_password = generate_secure_password()
# Get current secret
current = sm_client.get_secret_value(
SecretId=secret_id,
VersionStage='AWSCURRENT'
)
secret_dict = json.loads(current['SecretString'])
secret_dict['password'] = new_password
# Store as pending
sm_client.put_secret_value(
SecretId=secret_id,
ClientRequestToken=token,
SecretString=json.dumps(secret_dict),
VersionStages=['AWSPENDING']
)
elif step == 'setSecret':
# Update the actual resource (database, API, etc.)
pending = sm_client.get_secret_value(
SecretId=secret_id,
VersionStage='AWSPENDING'
)
new_creds = json.loads(pending['SecretString'])
# Update database password
update_database_password(new_creds)
elif step == 'testSecret':
# Verify new credentials work
pending = sm_client.get_secret_value(
SecretId=secret_id,
VersionStage='AWSPENDING'
)
new_creds = json.loads(pending['SecretString'])
test_database_connection(new_creds)
elif step == 'finishSecret':
# Promote pending to current
sm_client.update_secret_version_stage(
SecretId=secret_id,
VersionStage='AWSCURRENT',
MoveToVersionId=token,
RemoveFromVersionId=get_current_version(sm_client, secret_id)
)
def generate_secure_password(length=32):
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
return ''.join(secrets.choice(alphabet) for _ in range(length))Update and Manage Secrets
# Update secret value
aws secretsmanager update-secret \
--secret-id prod/api/stripe \
--secret-string '{"api_key":"sk_live_new","webhook_secret":"whsec_new"}'
# Add new key-value pair
aws secretsmanager update-secret \
--secret-id prod/api/stripe \
--secret-string "$(aws secretsmanager get-secret-value \
--secret-id prod/api/stripe \
--query SecretString \
--output text | jq '. + {"new_key":"new_value"}')"
# List secrets
aws secretsmanager list-secrets \
--query 'SecretList[*].[Name,LastChangedDate]' \
--output table
# Delete secret (with recovery window)
aws secretsmanager delete-secret \
--secret-id prod/old-secret \
--recovery-window-in-days 7
# Force delete immediately (no recovery)
aws secretsmanager delete-secret \
--secret-id prod/test-secret \
--force-delete-without-recoveryCross-Account Access
Share secrets across AWS accounts using resource policies:
# Allow another account to access secret
aws secretsmanager put-resource-policy \
--secret-id prod/shared/api-key \
--resource-policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222233334444:root"
},
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}]
}'Best Practices Summary
| Practice | Recommendation |
|---|---|
| Naming | Use hierarchical names (env/service/resource) |
| Rotation | Enable automatic rotation for all database secrets |
| Access | Use IAM roles, never embed credentials |
| Encryption | Use customer-managed KMS keys for sensitive secrets |
| Caching | Cache secrets in application memory |
| Audit | Monitor CloudTrail for secret access patterns |
| Tags | Tag secrets for cost allocation and organization |
Monitor Secret Usage
# CloudTrail query for secret access
aws logs filter-log-events \
--log-group-name /aws/cloudtrail/logs \
--filter-pattern '{ $.eventSource = "secretsmanager.amazonaws.com" }' \
--start-time $(date -d '1 day ago' +%s000)
# Create CloudWatch alarm for unauthorized access
aws cloudwatch put-metric-alarm \
--alarm-name SecretAccessDenied \
--metric-name CallCount \
--namespace AWS/SecretsManager \
--statistic Sum \
--period 300 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions Name=Operation,Value=GetSecretValue Name=ErrorCode,Value=AccessDeniedException \
--alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts