AWS Secrets Manager Tutorial: Store, Rotate, and Access Secrets

Complete tutorial on using AWS Secrets Manager to create secrets, set up automatic rotation, and securely access credentials from Lambda, EC2, and other AWS services.

10 min readUpdated 2026-01-13

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

  1. Open the Secrets Manager Console
  2. Click Store a new secret
  3. Choose secret type (database credentials, API key, or other)
  4. Enter secret values
  5. Name the secret (e.g., "prod/database/postgres")
  6. Configure rotation (optional)
  7. 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

# 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 production

Set 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-recovery

Cross-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

PracticeRecommendation
NamingUse hierarchical names (env/service/resource)
RotationEnable automatic rotation for all database secrets
AccessUse IAM roles, never embed credentials
EncryptionUse customer-managed KMS keys for sensitive secrets
CachingCache secrets in application memory
AuditMonitor CloudTrail for secret access patterns
TagsTag 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

Frequently Asked Questions

Find answers to common questions

Both store secrets, but Secrets Manager offers automatic rotation, cross-account access, and native database credential management. Parameter Store is simpler and cheaper (free tier available) but requires custom rotation logic. Use Secrets Manager for database credentials requiring rotation and Parameter Store for configuration values and simple secrets. Secrets Manager costs $0.40/secret/month plus $0.05 per 10,000 API calls.

Need Professional Help?

Our team of experts can help you implement and configure these solutions for your organization.