๐Ÿ”‘

AWS Secrets Manager

AWS Secrets Manager: create, rotate, replicate, and retrieve secrets; resource policies; Lambda rotation; and SDK patterns

Create & Manage Secrets

Creating, updating, tagging, and deleting secrets via the AWS CLI

bashยทCreate a secret
# Create a plain string secret
aws secretsmanager create-secret   --name myapp/prod/api-key   --description "Production API key for myapp"   --secret-string "s3cr3t-api-key"

# Create a JSON secret (recommended for structured data)
aws secretsmanager create-secret   --name myapp/prod/db   --description "Production database credentials"   --secret-string '{"username":"admin","password":"s3cr3t","host":"db.example.com","port":5432,"dbname":"myapp"}'

# Create from a file
aws secretsmanager create-secret   --name myapp/prod/tls-cert   --secret-string file://cert.pem

# Create with a customer-managed KMS key
aws secretsmanager create-secret   --name myapp/prod/db   --kms-key-id arn:aws:kms:us-east-1:123456789012:key/mrk-abc123   --secret-string '{"password":"s3cr3t"}'

# Create with tags
aws secretsmanager create-secret   --name myapp/prod/db   --secret-string '{"password":"s3cr3t"}'   --tags Key=env,Value=prod Key=team,Value=platform
bashยทUpdate & describe
# Update the secret value (creates a new version)
aws secretsmanager put-secret-value   --secret-id myapp/prod/db   --secret-string '{"username":"admin","password":"newpassword"}'

# Update with a client request token (idempotency key)
aws secretsmanager put-secret-value   --secret-id myapp/prod/db   --client-request-token "deploy-2024-01-01"   --secret-string '{"password":"newpass"}'

# Describe a secret (metadata, no value)
aws secretsmanager describe-secret   --secret-id myapp/prod/db

# List all secrets
aws secretsmanager list-secrets

# Filter by tag or name prefix
aws secretsmanager list-secrets   --filters Key=name,Values=myapp/prod Key=tag-key,Values=env

# List versions of a secret
aws secretsmanager list-secret-version-ids   --secret-id myapp/prod/db
bashยทDelete & restore
# Schedule deletion (default 30-day recovery window)
aws secretsmanager delete-secret   --secret-id myapp/prod/db

# Delete with custom recovery window (7โ€“30 days)
aws secretsmanager delete-secret   --secret-id myapp/prod/db   --recovery-window-in-days 7

# Delete immediately โ€” NO RECOVERY POSSIBLE
aws secretsmanager delete-secret   --secret-id myapp/prod/db   --force-delete-without-recovery

# Restore a secret scheduled for deletion
aws secretsmanager restore-secret   --secret-id myapp/prod/db

# Update tags
aws secretsmanager tag-resource   --secret-id myapp/prod/db   --tags Key=cost-center,Value=eng

# Remove a tag
aws secretsmanager untag-resource   --secret-id myapp/prod/db   --tag-keys cost-center

Retrieve Secrets

Getting secret values by name, ARN, version, or stage label

bashยทget-secret-value
# Get the current secret value
aws secretsmanager get-secret-value   --secret-id myapp/prod/db

# Extract just the secret string (JSON output)
aws secretsmanager get-secret-value   --secret-id myapp/prod/db   --query SecretString   --output text

# Parse a specific field from a JSON secret
aws secretsmanager get-secret-value   --secret-id myapp/prod/db   --query SecretString   --output text | jq -r '.password'

# Get a specific version by ID
aws secretsmanager get-secret-value   --secret-id myapp/prod/db   --version-id "abc123-uuid"

# Get by version stage label
aws secretsmanager get-secret-value   --secret-id myapp/prod/db   --version-stage AWSPREVIOUS

# Get a binary secret
aws secretsmanager get-secret-value   --secret-id myapp/prod/tls-key   --query SecretBinary   --output text | base64 --decode > tls.key
bashยทBatch & cross-region
# Retrieve from a specific region
aws secretsmanager get-secret-value   --secret-id myapp/prod/db   --region eu-west-1

# Use ARN instead of name (cross-account)
aws secretsmanager get-secret-value   --secret-id arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prod/db-AbCdEf

# Batch get multiple secrets (CLI loop pattern)
for secret in myapp/prod/db myapp/prod/api-key; do
  echo "=== $secret ==="
  aws secretsmanager get-secret-value     --secret-id "$secret"     --query SecretString     --output text
done

Secret Rotation

Configuring automatic and manual rotation with Lambda

bashยทEnable rotation
# Enable automatic rotation (every 30 days) using a managed Lambda
aws secretsmanager rotate-secret   --secret-id myapp/prod/db   --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRotation   --rotation-rules AutomaticallyAfterDays=30

# Enable rotation using a rotation schedule (cron expression)
aws secretsmanager rotate-secret   --secret-id myapp/prod/db   --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRotation   --rotation-rules '{"ScheduleExpression":"cron(0 2 1 * ? *)","Duration":"2h"}'

# Trigger an immediate rotation (test or emergency)
aws secretsmanager rotate-secret   --secret-id myapp/prod/db   --rotate-immediately

# Cancel rotation
aws secretsmanager cancel-rotate-secret   --secret-id myapp/prod/db

# Check rotation status
aws secretsmanager describe-secret   --secret-id myapp/prod/db   --query '{RotationEnabled:RotationEnabled,LastRotatedDate:LastRotatedDate,NextRotationDate:NextRotationDate}'
pythonยทRotation Lambda (Python)
"""
Lambda function template for Secrets Manager rotation.
Implements the 4-step rotation pattern required by AWS.
"""
import boto3
import json

client = boto3.client('secretsmanager')


def lambda_handler(event, context):
    arn = event['SecretId']
    token = event['ClientRequestToken']
    step = event['Step']

    metadata = client.describe_secret(SecretId=arn)
    if not metadata['RotationEnabled']:
        raise ValueError(f"Secret {arn} is not enabled for rotation")

    versions = metadata.get('VersionIdsToStages', {})
    if token not in versions:
        raise ValueError(f"Secret version {token} has no stage for arn {arn}")
    if 'AWSCURRENT' in versions[token]:
        return   # already current, nothing to do
    if 'AWSPENDING' not in versions[token]:
        raise ValueError(f"Secret version {token} is not set as AWSPENDING")

    if step == 'createSecret':
        create_secret(arn, token)
    elif step == 'setSecret':
        set_secret(arn, token)
    elif step == 'testSecret':
        test_secret(arn, token)
    elif step == 'finishSecret':
        finish_secret(arn, token)


def create_secret(arn, token):
    """Generate a new secret value and store as AWSPENDING."""
    try:
        client.get_secret_value(SecretId=arn, VersionStage='AWSPENDING',
                                VersionId=token)
        return  # already created
    except client.exceptions.ResourceNotFoundException:
        pass

    current = json.loads(
        client.get_secret_value(SecretId=arn,
                                VersionStage='AWSCURRENT')['SecretString']
    )
    current['password'] = generate_password()

    client.put_secret_value(
        SecretId=arn, ClientRequestToken=token,
        SecretString=json.dumps(current),
        VersionStages=['AWSPENDING'],
    )


def set_secret(arn, token):
    """Apply the AWSPENDING credentials to the database."""
    pending = json.loads(
        client.get_secret_value(SecretId=arn, VersionStage='AWSPENDING',
                                VersionId=token)['SecretString']
    )
    # TODO: ALTER USER in the database using pending credentials
    pass


def test_secret(arn, token):
    """Verify the AWSPENDING credentials work."""
    pending = json.loads(
        client.get_secret_value(SecretId=arn, VersionStage='AWSPENDING',
                                VersionId=token)['SecretString']
    )
    # TODO: Test a real connection with pending['password']
    pass


def finish_secret(arn, token):
    """Promote AWSPENDING to AWSCURRENT."""
    metadata = client.describe_secret(SecretId=arn)
    current_version = next(
        v for v, stages in metadata['VersionIdsToStages'].items()
        if 'AWSCURRENT' in stages
    )
    client.update_secret_version_stage(
        SecretId=arn, VersionStage='AWSCURRENT',
        MoveToVersionId=token, RemoveFromVersionId=current_version,
    )


def generate_password():
    return boto3.client('secretsmanager').get_random_password(
        PasswordLength=32, ExcludePunctuation=True
    )['RandomPassword']

Resource Policies

Granting cross-account access and least-privilege IAM policies

bashยทResource-based policy
# Attach a resource policy to a secret (enables cross-account access)
aws secretsmanager put-resource-policy   --secret-id myapp/prod/db   --resource-policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "AllowCrossAccountRead",
        "Effect": "Allow",
        "Principal": {
          "AWS": "arn:aws:iam::999888777666:role/AppRole"
        },
        "Action": [
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ],
        "Resource": "*"
      }
    ]
  }'

# Read the current resource policy
aws secretsmanager get-resource-policy   --secret-id myapp/prod/db

# Remove the resource policy
aws secretsmanager delete-resource-policy   --secret-id myapp/prod/db

# Validate the policy before attaching (dry-run)
aws secretsmanager validate-resource-policy   --resource-policy file://policy.json
jsonยทIAM identity policies
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadSpecificSecret",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prod/*"
    },
    {
      "Sid": "AllowKMSDecrypt",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/mrk-abc123"
    },
    {
      "Sid": "DenyOtherEnvs",
      "Effect": "Deny",
      "Action": "secretsmanager:*",
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/staging/*"
    }
  ]
}

Multi-Region Replication

Replicating secrets across AWS regions for HA and DR

bashยทReplicate secrets
# Replicate a secret to additional regions
aws secretsmanager replicate-secret-to-regions   --secret-id myapp/prod/db   --add-replica-regions     Region=eu-west-1     Region=ap-southeast-1

# Replicate with a region-specific KMS key
aws secretsmanager replicate-secret-to-regions   --secret-id myapp/prod/db   --add-replica-regions     Region=eu-west-1,KmsKeyId=arn:aws:kms:eu-west-1:123456789012:key/eu-key-id

# List replication status
aws secretsmanager describe-secret   --secret-id myapp/prod/db   --query ReplicationStatus

# Remove a replica region
aws secretsmanager remove-regions-from-replication   --secret-id myapp/prod/db   --remove-replica-regions eu-west-1

# Promote a replica to standalone (breaks replication)
aws secretsmanager stop-replication-to-replica   --secret-id arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/db-AbCdEf

SDK โ€” Python (boto3)

Retrieving and caching secrets in Python applications

pythonยทGet secret value
import boto3
import json
from botocore.exceptions import ClientError


def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
    client = boto3.client("secretsmanager", region_name=region)
    try:
        response = client.get_secret_value(SecretId=secret_name)
    except ClientError as e:
        error_code = e.response["Error"]["Code"]
        if error_code == "ResourceNotFoundException":
            raise ValueError(f"Secret {secret_name!r} not found") from e
        if error_code == "InvalidRequestException":
            raise ValueError(f"Invalid request for {secret_name!r}") from e
        if error_code == "DecryptionFailure":
            raise PermissionError("KMS decryption failed") from e
        if error_code == "AccessDeniedException":
            raise PermissionError("Access denied to secret") from e
        raise

    secret = response.get("SecretString")
    if secret:
        try:
            return json.loads(secret)
        except json.JSONDecodeError:
            return {"value": secret}
    # Binary secret
    import base64
    return {"value": base64.b64decode(response["SecretBinary"])}


# Usage
db_creds = get_secret("myapp/prod/db")
conn = psycopg2.connect(
    host=db_creds["host"],
    user=db_creds["username"],
    password=db_creds["password"],
    dbname=db_creds["dbname"],
)
pythonยทCaching with aws-secretsmanager-caching
# pip install aws-secretsmanager-caching
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig
import botocore.session
import json

# Configure cache: refresh every 1 hour, max 1000 secrets
config = SecretCacheConfig(
    max_cache_size=1000,
    exception_retry_delay_base=1,
    exception_retry_growth_factor=2,
    exception_retry_delay_max=3600,
    default_version_stage="AWSCURRENT",
    secret_refresh_interval=3600,        # seconds
    secret_version_stage_refresh_interval=3600,
)

session = botocore.session.get_session()
cache = SecretCache(config=config, client=session.create_client("secretsmanager"))


def get_secret_cached(secret_name: str) -> dict:
    secret_string = cache.get_secret_string(secret_name)
    return json.loads(secret_string)


# Subsequent calls within refresh_interval hit the in-process cache
db = get_secret_cached("myapp/prod/db")

SDK โ€” Node.js

Retrieving secrets in Node.js with the AWS SDK v3

typescriptยทGet secret (SDK v3)
// npm install @aws-sdk/client-secrets-manager
import {
  SecretsManagerClient,
  GetSecretValueCommand,
  ResourceNotFoundException,
} from "@aws-sdk/client-secrets-manager";

const client = new SecretsManagerClient({ region: "us-east-1" });

async function getSecret<T = Record<string, string>>(
  secretName: string
): Promise<T> {
  const command = new GetSecretValueCommand({ SecretId: secretName });

  try {
    const response = await client.send(command);

    if (response.SecretString) {
      return JSON.parse(response.SecretString) as T;
    }
    if (response.SecretBinary) {
      // Binary secret โ€” decode from Uint8Array
      const decoded = Buffer.from(response.SecretBinary).toString("utf-8");
      return JSON.parse(decoded) as T;
    }
    throw new Error("Secret has no value");
  } catch (err) {
    if (err instanceof ResourceNotFoundException) {
      throw new Error(`Secret "${secretName}" not found`);
    }
    throw err;
  }
}

// Usage
interface DbSecret {
  username: string;
  password: string;
  host: string;
  port: number;
  dbname: string;
}

const db = await getSecret<DbSecret>("myapp/prod/db");
console.log(`Connecting to ${db.host}:${db.port}/${db.dbname}`);
typescriptยทIn-process cache pattern
// Simple TTL cache to avoid hitting Secrets Manager on every request
const cache = new Map<string, { value: unknown; expiresAt: number }>();
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour

async function getSecretCached<T>(secretName: string): Promise<T> {
  const now = Date.now();
  const cached = cache.get(secretName);

  if (cached && cached.expiresAt > now) {
    return cached.value as T;
  }

  const value = await getSecret<T>(secretName);
  cache.set(secretName, { value, expiresAt: now + CACHE_TTL_MS });
  return value;
}

// Load all secrets at startup
async function loadSecrets() {
  const [db, apiKey] = await Promise.all([
    getSecretCached<DbSecret>("myapp/prod/db"),
    getSecretCached<{ key: string }>("myapp/prod/api-key"),
  ]);
  return { db, apiKey };
}

Integration Patterns

ECS, Lambda, EKS, and CloudFormation integration patterns

jsonยทECS task definition injection
{
  "family": "myapp",
  "taskRoleArn": "arn:aws:iam::123456789012:role/ECSTaskRole",
  "containerDefinitions": [
    {
      "name": "myapp",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest",
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prod/db:password::"
        },
        {
          "name": "DB_USERNAME",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prod/db:username::"
        },
        {
          "name": "API_KEY",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/prod/api-key::"
        }
      ],
      "environment": [
        { "name": "NODE_ENV", "value": "production" }
      ]
    }
  ]
}
yamlยทCloudFormation & SSM Parameter Store comparison
# CloudFormation โ€” reference a secret ARN (value is NOT resolved at deploy time)
# Use dynamic references to inject at runtime

Resources:
  MyDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      MasterUsername: '{{resolve:secretsmanager:myapp/prod/db:SecretString:username}}'
      MasterUserPassword: '{{resolve:secretsmanager:myapp/prod/db:SecretString:password}}'
      DBInstanceClass: db.t3.micro
      Engine: postgres

  # Secrets Manager vs SSM Parameter Store:
  # - Secrets Manager: automatic rotation, cross-account, $0.40/secret/month
  # - SSM SecureString: no native rotation, same-account only, free (standard) or $0.05/10k API calls
  # Rule of thumb: database creds / API keys โ†’ Secrets Manager
  #                config values / feature flags โ†’ SSM Parameter Store

# Lambda โ€” grant access via execution role
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      Policies:
        - PolicyName: SecretsAccess
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:myapp/prod/*'
yamlยทEKS โ€” External Secrets Operator
# External Secrets Operator syncs Secrets Manager โ†’ Kubernetes Secrets
# https://external-secrets.io

# SecretStore (per-namespace) โ€” uses IRSA (IAM Roles for Service Accounts)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secretsmanager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

---
# ExternalSecret โ€” maps Secrets Manager keys to a K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-db-creds
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secretsmanager
    kind: SecretStore
  target:
    name: myapp-db-creds       # name of the resulting K8s Secret
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD   # K8s Secret key
      remoteRef:
        key: myapp/prod/db     # Secrets Manager secret name
        property: password     # JSON field
    - secretKey: DB_USERNAME
      remoteRef:
        key: myapp/prod/db
        property: username