Docs / Secrets Manager

Secrets Manager

LocalEmu Secrets Manager implements all 23 operations. 20 of them are LocalEmu-patched: the gateway intercepts the call, runs custom validation and side-effect logic, then forwards to the moto-ext backend that stores the secret material. The remaining 3 (BatchGetSecretValue, GetRandomPassword, ListSecrets) pass through to moto unchanged. The standout feature is rotation: RotateSecret actually invokes your rotation Lambda four times (createSecret, setSecret, testSecret, finishSecret) in the same order and shape as AWS does, so you can validate the entire rotation lifecycle end to end before deploying.

Operation-level coverage: see the Secrets Manager coverage matrix.

Quick start

Terminal
$ awsemu secretsmanager create-secret --name prod/db/password \
    --secret-string '{"username":"appuser","password":"r3al-secret"}'
{
  "ARN":  "arn:aws:secretsmanager:us-east-1:000000000000:secret:prod/db/password-aBcDef",
  "Name": "prod/db/password",
  "VersionId": "11111111-2222-3333-4444-555555555555"
}

$ awsemu secretsmanager get-secret-value --secret-id prod/db/password \
    --query SecretString --output text
{"username":"appuser","password":"r3al-secret"}

$ awsemu secretsmanager update-secret --secret-id prod/db/password \
    --secret-string '{"username":"appuser","password":"new-rotated-value"}' \
    --query VersionId --output text
aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee

# The previous version is preserved with stage AWSPREVIOUS
$ awsemu secretsmanager list-secret-version-ids --secret-id prod/db/password \
    --query 'Versions[].{Id:VersionId,Stages:VersionStages}'
[
  {"Id": "11111111-2222-...", "Stages": ["AWSPREVIOUS"]},
  {"Id": "aaaaaaaa-bbbb-...", "Stages": ["AWSCURRENT"]}
]

Secret names are validated against ^[A-Za-z0-9/_+=.@-]+$ at services/secretsmanager/provider.py:139. Each CreateSecret or PutSecretValue requires a ClientRequestToken (auto-generated by the SDK when not supplied) which becomes the new VersionId.

Architecture

Code lives at services/secretsmanager/. The provider class SecretsmanagerProvider at provider.py:104 is a thin layer over the moto-ext backend (moto.secretsmanager.models.SecretsManagerBackend). Two patterns coexist:

Persistence works through the moto state registry. secretsmanager is registered in state/registry.py:41 as a tier-1 service (loads before Lambda and messaging). With PERSISTENCE=1 the entire backend, including every secret version, version stages, last-accessed dates, resource policies, and the queued rotation schedule, is dill-serialised on shutdown and restored on start.

Features supported

FeatureNotes
Secret lifecycleCreateSecret, DescribeSecret, UpdateSecret, DeleteSecret with recovery window or ForceDeleteWithoutRecovery, RestoreSecret.
ValuesPutSecretValue, GetSecretValue by VersionId or VersionStage. SecretString and SecretBinary both supported.
Version stagesAWSCURRENT, AWSPENDING, AWSPREVIOUS tracked. Single-AWSPREVIOUS invariant enforced. UpdateSecretVersionStage moves labels atomically.
ListSecretVersionIdsPaginated with MaxResults + NextToken. Per-version LastAccessedDate (date only, AWS shape).
RotationRotateSecret invokes the Lambda four times (createSecret, setSecret, testSecret, finishSecret). CancelRotateSecret tears down the schedule. AutomaticallyAfterDays in [1, 1000].
Resource policiesPutResourcePolicy, GetResourcePolicy, DeleteResourcePolicy, ValidateResourcePolicy.
TagsTagResource, UntagResource with secret-tag persistence across rotation.
ReplicationReplicateSecretToRegions, RemoveRegionsFromReplication, StopReplicationToReplica. Replica regions stored on the source; see Known limitations below for the data-copy nuance.
Cross-account KMS guardGetSecretValue and PutSecretValue reject cross-account access when the secret uses the default AWS-managed KMS key (provider.py:124-134).
Moto pass-throughBatchGetSecretValue, GetRandomPassword, ListSecrets delegate straight to moto with no LocalEmu changes.
PersistenceTier-1 in state/registry.py:41. All secrets, versions, stages, policies, tags, and rotation schedules restored when PERSISTENCE=1.

Rotation with a real Lambda

RotateSecret at provider.py:774-906 validates the rotation Lambda via GetFunction, then drives the AWS-standard four-step protocol against it. The Lambda receives a payload {Step, SecretId, ClientRequestToken} for each call. If any step returns a FunctionError, the rotation is aborted and the secret is left in the AWSPENDING state for inspection.

Terminal
# 1. Write a rotation function. AWS requires 4 step handlers:
#    createSecret, setSecret, testSecret, finishSecret.
$ cat > rotate.py <<'PY'
import boto3, json, os
sm = boto3.client("secretsmanager", endpoint_url="http://host.docker.internal:4566")

def lambda_handler(event, _):
    step = event["Step"]
    sid  = event["SecretId"]
    new_token = event["ClientRequestToken"]
    if step == "createSecret":
        sm.put_secret_value(
            SecretId=sid, ClientRequestToken=new_token,
            SecretString=json.dumps({"password": f"rotated-{new_token[:8]}"}),
            VersionStages=["AWSPENDING"],
        )
    elif step == "finishSecret":
        cur = next(v["VersionId"] for v in sm.list_secret_version_ids(SecretId=sid)["Versions"]
                   if "AWSCURRENT" in v["VersionStages"])
        sm.update_secret_version_stage(
            SecretId=sid, VersionStage="AWSCURRENT",
            MoveToVersionId=new_token, RemoveFromVersionId=cur,
        )
PY

$ zip rotate.zip rotate.py
$ awsemu lambda create-function --function-name rotator \
    --runtime python3.12 --handler rotate.lambda_handler \
    --role arn:aws:iam::000000000000:role/lambda-role \
    --zip-file fileb://rotate.zip --query FunctionArn --output text
arn:aws:lambda:us-east-1:000000000000:function:rotator

$ awsemu secretsmanager rotate-secret --secret-id prod/db/password \
    --rotation-lambda-arn arn:aws:lambda:us-east-1:000000000000:function:rotator \
    --rotation-rules AutomaticallyAfterDays=30 \
    --query VersionId --output text
bbbbbbbb-cccc-dddd-eeee-ffffffffffff

# Verify rotation actually ran the Lambda (4 invocations, one per step)
$ awsemu secretsmanager get-secret-value --secret-id prod/db/password \
    --query SecretString --output text
{"password": "rotated-bbbbbbbb"}

Stuck-rotation detection is built in (provider.py:836-844): if a subsequent RotateSecret finds AWSPENDING attached to a version that is not also AWSCURRENT, the call returns InvalidRequestException with the message Previous rotation request is still in progress., matching AWS behaviour.

Version stages and rollback

Every successful write creates a new version with an explicit VersionId. The version graph keeps the prior AWSCURRENT tagged AWSPREVIOUS so a one-line rollback is always possible. Old versions with no stages are cleaned up automatically once 100 of them accumulate (MAX_OUTDATED_SECRET_VERSIONS=100 at provider.py:79, cleanup at provider.py:737-743).

Terminal
# Promote the previous version back to AWSCURRENT (rollback)
$ CURRENT=$(awsemu secretsmanager list-secret-version-ids --secret-id prod/db/password \
    --query 'Versions[?contains(VersionStages, `AWSCURRENT`)].VersionId | [0]' --output text)
$ PREV=$(awsemu secretsmanager list-secret-version-ids --secret-id prod/db/password \
    --query 'Versions[?contains(VersionStages, `AWSPREVIOUS`)].VersionId | [0]' --output text)

$ awsemu secretsmanager update-secret-version-stage --secret-id prod/db/password \
    --version-stage AWSCURRENT \
    --remove-from-version-id $CURRENT --move-to-version-id $PREV

$ awsemu secretsmanager get-secret-value --secret-id prod/db/password \
    --version-stage AWSCURRENT --query SecretString --output text
{"username":"appuser","password":"r3al-secret"}

Resource policy management

The full resource-policy API is wired (PutResourcePolicy, GetResourcePolicy, DeleteResourcePolicy, ValidateResourcePolicy). The validator runs moto's IAM-policy syntax checker and returns PolicyValidationPassed. Policies are stored on the secret and returned verbatim. See Known limitations for the access-control enforcement note.

Terminal
$ awsemu secretsmanager put-resource-policy --secret-id prod/db/password \
    --resource-policy '{
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": {"AWS": "arn:aws:iam::000000000000:role/AppRole"},
        "Action":    "secretsmanager:GetSecretValue",
        "Resource":  "*"
      }]
    }'

$ awsemu secretsmanager get-resource-policy --secret-id prod/db/password \
    --query ResourcePolicy --output text | python3 -m json.tool
{"Version": "2012-10-17", "Statement": [{...}]}

$ awsemu secretsmanager validate-resource-policy --secret-id prod/db/password \
    --resource-policy file://policy.json --query PolicyValidationPassed
true

Integration points

Other LocalEmu services fetch secrets from this backend.

CallerWhere
EventBridge API destinationsservices/events/api_destination.py: HTTP auth secret fetched at dispatch time and injected as the configured header.
EventBridge connectionsservices/events/connection.py: custom auth parameter resolution.
CloudFormation parameter resolutionservices/cloudformation/engine/v2/resolving.py: resolves {{resolve:secretsmanager:...}} dynamic references during stack create/update.
CloudFormation dynamic refsservices/cloudformation/engine/template_deployer.py: legacy v1 engine path.
SSMservices/ssm/provider.py: GetParameter with type SecureString referencing a Secrets Manager ARN.

Configuration

Secrets Manager has no service-specific environment variables. Persistence is enabled by the generic PERSISTENCE=1 flag; rotation reuses the standard Lambda invocation path (so anything that affects Lambda, e.g. LAMBDA_DOCKER_NETWORK, applies to rotation calls).

Known limitations