tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.List app directory contents
ls -la /app/
total 28 drwxr-xr-x 1 root root 4096 Apr 22 19:10 . drwxr-xr-x 1 root root 4096 Apr 22 19:41 .. -rw-r--r-- 1 root root 4137 Apr 22 19:10 handler.py -rwxr-xr-x 1 root root 6097 Apr 22 19:10 setup.sh drwxr-xr-x 2 root root 4096 Apr 22 19:10 starter [stdout] total 28 drwxr-xr-x 1 root root 4096 Apr 22 19:10 . drwxr-xr-x 1 root root 4096 Apr 22 19:41 .. -rw-r--r-- 1 root root 4137 Apr 22 19:10 handler.py -rwxr-xr-x 1 root root 6097 Apr 22 19:10 setup.sh drwxr-xr-x 2 root root 4096 Apr 22 19:10 starter
/app/handler.py
1 # Rotation Lambda for AWS Secrets Manager, "single-user" pattern
2 # (no external database, value is self-contained).
3 #
4 # Secrets Manager calls this function 4 times in sequence per rotation,
5 # passing a Step field: createSecret, setSecret, testSecret, finishSecret.
6 # Each step must return cleanly for the rotation to advance.
7 #
8 # Reference:
9 # https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda-functions.html
10
11 import json
12 import logging
13 import os
14
15 import boto3
16
17 logger = logging.getLogger()
18 logger.setLevel(logging.INFO)
19
20 ENDPOINT = os.environ.get("SECRETS_MANAGER_ENDPOINT") or os.environ.get(
21 "AWS_ENDPOINT_URL"
22 )
23
24
25 def _client():
26 return boto3.client("secretsmanager", endpoint_url=ENDPOINT) if ENDPOINT else boto3.client("secretsmanager")
27
28
29 def lambda_handler(event, context):
30 arn = event["SecretId"]
31 token = event["ClientRequestToken"]
32 step = event["Step"]
33
34 client = _client()
35
36 desc = client.describe_secret(SecretId=arn)
37 if not desc.get("RotationEnabled"):
38 logger.error("Secret %s is not enabled for rotation", arn)
39 raise ValueError(f"Secret {arn} is not enabled for rotation")
40
41 versions = desc.get("VersionIdsToStages", {})
42 if token not in versions:
43 logger.error("Secret version %s has no stage for rotation of %s", token, arn)
44 raise ValueError(f"Secret version {token} has no stage for rotation of secret {arn}")
45 if "AWSCURRENT" in versions[token]:
46 logger.info("Secret version %s already AWSCURRENT for %s", token, arn)
47 return
48 if "AWSPENDING" not in versions[token]:
49 logger.error("Secret version %s not staged as AWSPENDING for %s", token, arn)
50 raise ValueError(f"Secret version {token} not set as AWSPENDING for rotation of secret {arn}")
51
52 if step == "createSecret":
53 create_secret(client, arn, token)
54 elif step == "setSecret":
55 set_secret(client, arn, token)
56 elif step == "testSecret":
57 test_secret(client, arn, token)
58 elif step == "finishSecret":
59 finish_secret(client, arn, token)
60 else:
61 raise ValueError(f"Invalid step parameter: {step}")
62
63
64 def create_secret(client, arn, token):
65 # Generate a new candidate value and stash it as AWSPENDING.
66 client.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT")
67
68 try:
69 client.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING")
70 logger.info("createSecret: pending version %s already exists", token)
71 except client.exceptions.ResourceNotFoundException:
72 password = client.get_random_password(PasswordLength=32, ExcludePunctuation=True)
73 client.put_secret_value(
74 SecretId=arn,
75 ClientRequestToken=token,
76 SecretString=json.dumps({"password": password["RandomPassword"]}),
77 VersionStages=["AWSPENDING"],
78 )
79 logger.info("createSecret: stored new AWSPENDING version %s", token)
80
81
82 def set_secret(client, arn, token):
83 # No external system to update for a self-contained secret; validate
84 # that both AWSCURRENT and AWSPENDING are readable and move on.
85 client.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT")
86 client.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING")
87 logger.info("setSecret: both stages readable")
88
89
90 def test_secret(client, arn, token):
91 # Sanity-check the pending value parses as JSON with a non-empty
92 # password field. Real handlers would attempt a login here.
93 pending = client.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING")
94 parsed = json.loads(pending["SecretString"])
95 if not parsed.get("password"):
96 raise ValueError("testSecret: pending value has no password")
97 logger.info("testSecret: pending value validated")
98
99
100 def finish_secret(client, arn, token):
101 # TODO: promote the AWSPENDING version to AWSCURRENT here.
102 # Without this, rotation will appear to succeed from Secrets
103 # Manager's orchestrator but AWSCURRENT never advances and
104 # consumers keep reading the old password.
105 logger.info("finishSecret: (no-op)")
106
/app/setup.sh
1 #!/bin/bash
2 # Seeds LocalStack with the broken-by-design state the agent inherits:
3 # - customer-managed KMS CMK (alias alias/app-rotation-key)
4 # - Secrets Manager secret encrypted with that CMK
5 # - IAM role for the rotation Lambda (with deliberately incomplete policy)
6 # - Rotation Lambda (with a subtle bug in finishSecret)
7 # - Rotation configured on the secret, pointing at the Lambda
8 #
9 # The task expects these resources to already exist when the agent starts
10 # working. The agent must find the bugs and make `rotate-secret` actually
11 # advance AWSCURRENT end-to-end.
12
13 set -euo pipefail
14
15 REGION="${AWS_DEFAULT_REGION:-us-east-1}"
16 ACCOUNT_ID="000000000000"
17 SECRET_NAME="app/db/password"
18 ROLE_NAME="secrets-rotator-role"
19 FUNCTION="secrets-rotator"
20 KEY_ALIAS="alias/app-rotation-key"
21
22 log() { echo "[setup] $*" >&2; }
23
24 log "waiting for localstack health..."
25 for _ in $(seq 1 60); do
26 if curl -sf http://localstack:4566/_localstack/health | grep -q '"secretsmanager": "available"'; then
27 break
28 fi
29 sleep 2
30 done
31
32 # 1. CMK with a minimal key policy (root admin only , no grant for the
33 # rotation Lambda role yet).
34 log "creating KMS CMK"
35 KEY_POLICY=$(cat <<JSON
36 {
37 "Version": "2012-10-17",
38 "Id": "app-rotation-key-policy",
39 "Statement": [
40 {
41 "Sid": "EnableRootAdmin",
42 "Effect": "Allow",
43 "Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:root" },
44 "Action": "kms:*",
45 "Resource": "*"
46 }
47 ]
48 }
49 JSON
50 )
51 KEY_ID=$(aws kms create-key \
52 --description "Customer CMK for app/db/password" \
53 --key-usage ENCRYPT_DECRYPT \
54 --policy "$KEY_POLICY" \
55 --query 'KeyMetadata.KeyId' --output text)
56 aws kms create-alias --alias-name "$KEY_ALIAS" --target-key-id "$KEY_ID" >/dev/null
57 KEY_ARN="arn:aws:kms:${REGION}:${ACCOUNT_ID}:key/${KEY_ID}"
58 log "created CMK $KEY_ID"
59
60 # 2. Secret, encrypted with the CMK.
61 log "creating secret"
62 aws secretsmanager create-secret \
63 --name "$SECRET_NAME" \
64 --kms-key-id "$KEY_ARN" \
65 --secret-string '{"password": "initial-placeholder-value"}' \
66 --description "App DB password, rotated by Lambda" >/dev/null
67
68 SECRET_ARN=$(aws secretsmanager describe-secret --secret-id "$SECRET_NAME" \
69 --query 'ARN' --output text)
70 log "created secret $SECRET_ARN"
71
72 # 3. IAM role for the rotation Lambda. Deliberately incomplete , has
73 # secretsmanager:* but no KMS actions. The broken state the agent
74 # inherits.
75 log "creating rotation Lambda role"
76 TRUST=$(cat <<'JSON'
77 {
78 "Version": "2012-10-17",
79 "Statement": [
80 {"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}
81 ]
82 }
83 JSON
84 )
85 aws iam create-role \
86 --role-name "$ROLE_NAME" \
87 --assume-role-policy-document "$TRUST" >/dev/null
88
89 aws iam attach-role-policy \
90 --role-name "$ROLE_NAME" \
91 --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
92
93 INLINE=$(cat <<JSON
94 {
95 "Version": "2012-10-17",
96 "Statement": [
97 {
98 "Effect": "Allow",
99 "Action": [
100 "secretsmanager:DescribeSecret",
101 "secretsmanager:GetSecretValue",
102 "secretsmanager:PutSecretValue",
103 "secretsmanager:UpdateSecretVersionStage"
104 ],
105 "Resource": "${SECRET_ARN}"
106 },
107 {
108 "Effect": "Allow",
109 "Action": "secretsmanager:GetRandomPassword",
110 "Resource": "*"
111 }
112 ]
113 }
114 JSON
115 )
116 aws iam put-role-policy \
117 --role-name "$ROLE_NAME" \
118 --policy-name SecretsRotatorPolicy \
119 --policy-document "$INLINE"
120
121 ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.Arn' --output text)
122 log "created role $ROLE_ARN"
123
124 # 4. Package + deploy the rotation Lambda (with its buggy handler).
125 log "packaging rotation Lambda"
126 WORKDIR="$(mktemp -d)"
127 cp /app/handler.py "${WORKDIR}/handler.py"
128 (cd "$WORKDIR" && zip -q handler.zip handler.py)
129
130 aws lambda create-function \
131 --function-name "$FUNCTION" \
132 --runtime python3.11 \
133 --role "$ROLE_ARN" \
134 --handler handler.lambda_handler \
135 --timeout 30 \
136 --memory-size 256 \
137 --environment "Variables={SECRETS_MANAGER_ENDPOINT=http://localstack:4566}" \
138 --zip-file "fileb://${WORKDIR}/handler.zip" >/dev/null
139
140 for _ in $(seq 1 30); do
141 STATE=$(aws lambda get-function --function-name "$FUNCTION" \
142 --query 'Configuration.State' --output text 2>/dev/null || echo "Pending")
143 [ "$STATE" = "Active" ] && break
144 sleep 1
145 done
146 log "Lambda $FUNCTION active"
147
148 # NOTE: deliberately NOT calling `aws lambda add-permission` with
149 # principal secretsmanager.amazonaws.com. The agent must add that.
150
151 # 5. Attach rotation config on the secret. Secrets Manager refuses to
152 # attach unless the rotation Lambda already grants it InvokeFunction,
153 # so we temporarily add that permission, attach rotation, then remove
154 # the permission so the agent still has to re-add it as part of their
155 # fix. Net result: RotationEnabled=true but rotation fails at runtime
156 # because of the other broken layers (KMS grants, handler bug, Lambda
157 # resource policy).
158 log "attaching rotation config (temp Lambda permission)"
159 FUNCTION_ARN="arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:${FUNCTION}"
160 aws lambda add-permission \
161 --function-name "$FUNCTION" \
162 --statement-id TempRotationSetup \
163 --action lambda:InvokeFunction \
164 --principal secretsmanager.amazonaws.com \
165 --source-arn "$SECRET_ARN" >/dev/null 2>&1 || true
166
167 if aws secretsmanager rotate-secret \
168 --secret-id "$SECRET_NAME" \
169 --rotation-lambda-arn "$FUNCTION_ARN" \
170 --rotation-rules AutomaticallyAfterDays=30 \
171 --no-rotate-immediately >/dev/null 2>&1; then
172 log "rotation attached (no immediate trigger)"
173 elif aws secretsmanager rotate-secret \
174 --secret-id "$SECRET_NAME" \
175 --rotation-lambda-arn "$FUNCTION_ARN" \
176 --rotation-rules AutomaticallyAfterDays=30 >/dev/null 2>&1; then
177 log "rotation attached (first run may have fired)"
178 else
179 log "rotate-secret failed even with temp permission , check LocalStack compat"
180 fi
181
182 # Strip the temp permission so the agent still has to add it.
183 aws lambda remove-permission \
184 --function-name "$FUNCTION" \
185 --statement-id TempRotationSetup >/dev/null 2>&1 || true
186
187 log "done"
188
Inspect secret
aws secretsmanager describe-secret --secret-id app/db/password
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"Description": "App DB password, rotated by Lambda",
"KmsKeyId": "arn:aws:kms:us-east-1:000000000000:key/f559f8aa-d6b9-4364-9b56-83935f1be99f",
"RotationEnabled": true,
"RotationLambdaARN": "arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator",
"RotationRules": {
"AutomaticallyAfterDays": 30
},
"LastRotatedDate": "2026-04-22T19:41:11+00:00",
"LastChangedDate": "2026-04-22T19:41:08.037000+00:00",
"LastAccessedDate": "2026-04-22T00:00:00+00:00",
"NextRotationDate": "2026-05-22T19:41:06+00:00",
"VersionIdsToStages": {
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSCURRENT"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING"
]
},
"CreatedDate": "2026-04-22T19:40:58.007729+00:00"
}
[stdout]
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"Description": "App DB password, rotated by Lambda",
"KmsKeyId": "arn:aws:kms:us-east-1:000000000000:key/f559f8aa-d6b9-4364-9b56-83935f1be99f",
"RotationEnabled": true,
"RotationLambdaARN": "arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator",
"RotationRules": {
"AutomaticallyAfterDays": 30
},
"LastRotatedDate": "2026-04-22T19:41:11+00:00",
"LastChangedDate": "2026-04-22T19:41:08.037000+00:00",
"LastAccessedDate": "2026-04-22T00:00:00+00:00",
"NextRotationDate": "2026-05-22T19:41:06+00:00",
"VersionIdsToStages": {
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSCURRENT"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING"
]
},
"CreatedDate": "2026-04-22T19:40:58.007729+00:00"
}Try rotation to see error
aws secretsmanager rotate-secret --secret-id app/db/password 2>&1
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"VersionId": "39e9631b-5925-4950-ab75-2ab447df5e0e"
}
[stdout]
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"VersionId": "39e9631b-5925-4950-ab75-2ab447df5e0e"
}Check secret stages after rotate
sleep 3; aws secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages'
{
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSCURRENT"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING"
]
}
[stdout]
{
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSCURRENT"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING"
]
}Check Lambda resource policy and config
aws lambda get-policy --function-name secrets-rotator 2>&1; echo "---"; aws lambda get-function-configuration --function-name secrets-rotator
aws: [ERROR]: An error occurred (ResourceNotFoundException) when calling the GetPolicy operation: The resource you requested does not exist.
Additional error details:
Type: User
---
{
"FunctionName": "secrets-rotator",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator",
"Runtime": "python3.11",
"Role": "arn:aws:iam::000000000000:role/secrets-rotator-role",
"Handler": "handler.lambda_handler",
"CodeSize": 1594,
"Description": "",
"Timeout": 30,
"MemorySize": 256,
"LastModified": "2026-04-22T19:41:00.511929+0000",
"CodeSha256": "EUrzkxhJtdROVWMEENCirFXkGEqBUf7gWZMGFw0b83o=",
"Version": "$LATEST",
"Environment": {
"Variables": {
"SECRETS_MANAGER_ENDPOINT": "http://localstack:4566"
}
},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "59fbe9f4-78bc-4dcf-90c1-cf360fab2116",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
},
"RuntimeVersionConfig": {
"RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:8eeff65f6809a3ce81507fe733fe09b835899b99481ba22fd75b5a7338290ec1"
},
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/secrets-rotator"
}
}
[stdout]
aws: [ERROR]: An error occurred (ResourceNotFoundException) when calling the GetPolicy operation: The resource you requested does not exist.
Additional error details:
Type: User
---
{
"FunctionName": "secrets-rotator",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator",
"Runtime": "python3.11",
"Role": "arn:aws:iam::000000000000:role/secrets-rotator-role",
"Handler": "handler.lambda_handler",
"CodeSize": 1594,
"Description": "",
"Timeout": 30,
"MemorySize": 256,
"LastModified": "2026-04-22T19:41:00.511929+0000",
"CodeSha256": "EUrzkxhJtdROVWMEENCirFXkGEqBUf7gWZMGFw0b83o=",
"Version": "$LATEST",
"Environment": {
"Variables": {
"SECRETS_MANAGER_ENDPOINT": "http://localstack:4566"
}
},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "59fbe9f4-78bc-4dcf-90c1-cf360fab2116",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
},
"RuntimeVersionConfig": {
"RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:8eeff65f6809a3ce81507fe733fe09b835899b99481ba22fd75b5a7338290ec1"
},
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/secrets-rotator"
}
}Inspect KMS key policy and grants
KEY_ID=$(aws kms describe-key --key-id alias/app-rotation-key --query 'KeyMetadata.KeyId' --output text); echo "KEY_ID=$KEY_ID"; aws kms get-key-policy --key-id "$KEY_ID" --policy-name default --output text; echo "---"; aws kms list-grants --key-id "$KEY_ID"
KEY_ID=f559f8aa-d6b9-4364-9b56-83935f1be99f
{
"Version": "2012-10-17",
"Id": "app-rotation-key-policy",
"Statement": [
{
"Sid": "EnableRootAdmin",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::000000000000:root" },
"Action": "kms:*",
"Resource": "*"
}
]
}
---
{
"Grants": []
}
[stdout]
KEY_ID=f559f8aa-d6b9-4364-9b56-83935f1be99f
{
"Version": "2012-10-17",
"Id": "app-rotation-key-policy",
"Statement": [
{
"Sid": "EnableRootAdmin",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::000000000000:root" },
"Action": "kms:*",
"Resource": "*"
}
]
}
---
{
"Grants": []
}Check inline role policy
aws iam get-role-policy --role-name secrets-rotator-role --policy-name SecretsRotatorPolicy
{
"RoleName": "secrets-rotator-role",
"PolicyName": "SecretsRotatorPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
],
"Resource": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
}
]
}
}
[stdout]
{
"RoleName": "secrets-rotator-role",
"PolicyName": "SecretsRotatorPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
],
"Resource": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
}
]
}
}{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Look at Lambda logs
aws logs describe-log-groups --log-group-name-prefix /aws/lambda/secrets-rotator 2>&1 | head -20; echo "---"; aws logs tail /aws/lambda/secrets-rotator --since 10m 2>&1 | tail -40
{
"logGroups": [
{
"logGroupName": "/aws/lambda/secrets-rotator",
"creationTime": 1776886868092,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/secrets-rotator:*",
"storedBytes": 3935,
"logGroupClass": "STANDARD",
"logGroupArn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/secrets-rotator"
}
]
}
---
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 START RequestId: 274740df-c5ea-40d2-a3c9-3c3c97b5487b Version: $LATEST
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 [INFO] 2026-04-22T19:41:07.857Z 274740df-c5ea-40d2-a3c9-3c3c97b5487b Found credentials in environment variables.
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 [INFO] 2026-04-22T19:41:08.038Z 274740df-c5ea-40d2-a3c9-3c3c97b5487b createSecret: stored new AWSPENDING version 39e9631b-5925-4950-ab75-2ab447df5e0e
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 END RequestId: 274740df-c5ea-40d2-a3c9-3c3c97b5487b
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 REPORT RequestId: 274740df-c5ea-40d2-a3c9-3c3c97b5487b Duration: 236.18 ms Billed Duration: 237 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 START RequestId: 30ab8ade-9414-477a-80ce-ca0ca9995f67 Version: $LATEST
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 [INFO] 2026-04-22T19:41:09.007Z 30ab8ade-9414-477a-80ce-ca0ca9995f67 Found credentials in environment variables.
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 [INFO] 2026-04-22T19:41:09.169Z 30ab8ade-9414-477a-80ce-ca0ca9995f67 setSecret: both stages readable
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 END RequestId: 30ab8ade-9414-477a-80ce-ca0ca9995f67
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 REPORT RequestId: 30ab8ade-9414-477a-80ce-ca0ca9995f67 Duration: 215.21 ms Billed Duration: 216 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 START RequestId: 5c2acda0-e903-4b37-9765-866a69e9b62c Version: $LATEST
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 [INFO] 2026-04-22T19:41:10.106Z 5c2acda0-e903-4b37-9765-866a69e9b62c Found credentials in environment variables.
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 [INFO] 2026-04-22T19:41:10.279Z 5c2acda0-e903-4b37-9765-866a69e9b62c testSecret: pending value validated
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 END RequestId: 5c2acda0-e903-4b37-9765-866a69e9b62c
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 REPORT RequestId: 5c2acda0-e903-4b37-9765-866a69e9b62c Duration: 225.69 ms Billed Duration: 226 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb START RequestId: b42188c3-de39-4a53-bf90-e67f35565f46 Version: $LATEST
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb [INFO] 2026-04-22T19:41:11.341Z b42188c3-de39-4a53-bf90-e67f35565f46 Found credentials in environment variables.
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb [INFO] 2026-04-22T19:41:11.498Z b42188c3-de39-4a53-bf90-e67f35565f46 finishSecret: (no-op)
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb END RequestId: b42188c3-de39-4a53-bf90-e67f35565f46
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb REPORT RequestId: b42188c3-de39-4a53-bf90-e67f35565f46 Duration: 211.57 ms Billed Duration: 212 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 START RequestId: 9cf983a1-292b-4109-9c00-4a8e720fe1cc Version: $LATEST
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 [INFO] 2026-04-22T19:42:52.149Z 9cf983a1-292b-4109-9c00-4a8e720fe1cc Found credentials in environment variables.
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 [INFO] 2026-04-22T19:42:52.321Z 9cf983a1-292b-4109-9c00-4a8e720fe1cc createSecret: pending version 39e9631b-5925-4950-ab75-2ab447df5e0e already exists
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 END RequestId: 9cf983a1-292b-4109-9c00-4a8e720fe1cc
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 REPORT RequestId: 9cf983a1-292b-4109-9c00-4a8e720fe1cc Duration: 224.34 ms Billed Duration: 225 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be START RequestId: 756edb9d-08fb-418c-a223-6490b4f5b0ac Version: $LATEST
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be [INFO] 2026-04-22T19:42:53.279Z 756edb9d-08fb-418c-a223-6490b4f5b0ac Found credentials in environment variables.
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be [INFO] 2026-04-22T19:42:53.441Z 756edb9d-08fb-418c-a223-6490b4f5b0ac setSecret: both stages readable
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be END RequestId: 756edb9d-08fb-418c-a223-6490b4f5b0ac
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be REPORT RequestId: 756edb9d-08fb-418c-a223-6490b4f5b0ac Duration: 213.76 ms Billed Duration: 214 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 START RequestId: dca238f6-9391-4b23-8123-095110338c07 Version: $LATEST
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 [INFO] 2026-04-22T19:42:54.468Z dca238f6-9391-4b23-8123-095110338c07 Found credentials in environment variables.
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 [INFO] 2026-04-22T19:42:54.627Z dca238f6-9391-4b23-8123-095110338c07 testSecret: pending value validated
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 END RequestId: dca238f6-9391-4b23-8123-095110338c07
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 REPORT RequestId: dca238f6-9391-4b23-8123-095110338c07 Duration: 210.42 ms Billed Duration: 211 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 START RequestId: 84188510-a9d9-48af-9e6d-fe14febe4980 Version: $LATEST
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 [INFO] 2026-04-22T19:42:55.590Z 84188510-a9d9-48af-9e6d-fe14febe4980 Found credentials in environment variables.
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 [INFO] 2026-04-22T19:42:55.741Z 84188510-a9d9-48af-9e6d-fe14febe4980 finishSecret: (no-op)
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 END RequestId: 84188510-a9d9-48af-9e6d-fe14febe4980
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 REPORT RequestId: 84188510-a9d9-48af-9e6d-fe14febe4980 Duration: 202.32 ms Billed Duration: 203 ms Memory Size: 256 MB Max Memory Used: 256 MB
[stdout]
{
"logGroups": [
{
"logGroupName": "/aws/lambda/secrets-rotator",
"creationTime": 1776886868092,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/secrets-rotator:*",
"storedBytes": 3935,
"logGroupClass": "STANDARD",
"logGroupArn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/secrets-rotator"
}
]
}
---
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 START RequestId: 274740df-c5ea-40d2-a3c9-3c3c97b5487b Version: $LATEST
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 [INFO] 2026-04-22T19:41:07.857Z 274740df-c5ea-40d2-a3c9-3c3c97b5487b Found credentials in environment variables.
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 [INFO] 2026-04-22T19:41:08.038Z 274740df-c5ea-40d2-a3c9-3c3c97b5487b createSecret: stored new AWSPENDING version 39e9631b-5925-4950-ab75-2ab447df5e0e
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 END RequestId: 274740df-c5ea-40d2-a3c9-3c3c97b5487b
2026-04-22T19:41:08.043000+00:00 2026/04/22/[$LATEST]94f51c98861627983fd822b1592c3875 REPORT RequestId: 274740df-c5ea-40d2-a3c9-3c3c97b5487b Duration: 236.18 ms Billed Duration: 237 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 START RequestId: 30ab8ade-9414-477a-80ce-ca0ca9995f67 Version: $LATEST
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 [INFO] 2026-04-22T19:41:09.007Z 30ab8ade-9414-477a-80ce-ca0ca9995f67 Found credentials in environment variables.
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 [INFO] 2026-04-22T19:41:09.169Z 30ab8ade-9414-477a-80ce-ca0ca9995f67 setSecret: both stages readable
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 END RequestId: 30ab8ade-9414-477a-80ce-ca0ca9995f67
2026-04-22T19:41:09.174000+00:00 2026/04/22/[$LATEST]69b5695965878bf61b48c7f166e3ccd5 REPORT RequestId: 30ab8ade-9414-477a-80ce-ca0ca9995f67 Duration: 215.21 ms Billed Duration: 216 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 START RequestId: 5c2acda0-e903-4b37-9765-866a69e9b62c Version: $LATEST
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 [INFO] 2026-04-22T19:41:10.106Z 5c2acda0-e903-4b37-9765-866a69e9b62c Found credentials in environment variables.
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 [INFO] 2026-04-22T19:41:10.279Z 5c2acda0-e903-4b37-9765-866a69e9b62c testSecret: pending value validated
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 END RequestId: 5c2acda0-e903-4b37-9765-866a69e9b62c
2026-04-22T19:41:10.284000+00:00 2026/04/22/[$LATEST]b38927da2e8c49dcf74cf248ef848378 REPORT RequestId: 5c2acda0-e903-4b37-9765-866a69e9b62c Duration: 225.69 ms Billed Duration: 226 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb START RequestId: b42188c3-de39-4a53-bf90-e67f35565f46 Version: $LATEST
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb [INFO] 2026-04-22T19:41:11.341Z b42188c3-de39-4a53-bf90-e67f35565f46 Found credentials in environment variables.
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb [INFO] 2026-04-22T19:41:11.498Z b42188c3-de39-4a53-bf90-e67f35565f46 finishSecret: (no-op)
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb END RequestId: b42188c3-de39-4a53-bf90-e67f35565f46
2026-04-22T19:41:11.503000+00:00 2026/04/22/[$LATEST]798c47acba794eb943cc02751ce20dbb REPORT RequestId: b42188c3-de39-4a53-bf90-e67f35565f46 Duration: 211.57 ms Billed Duration: 212 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 START RequestId: 9cf983a1-292b-4109-9c00-4a8e720fe1cc Version: $LATEST
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 [INFO] 2026-04-22T19:42:52.149Z 9cf983a1-292b-4109-9c00-4a8e720fe1cc Found credentials in environment variables.
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 [INFO] 2026-04-22T19:42:52.321Z 9cf983a1-292b-4109-9c00-4a8e720fe1cc createSecret: pending version 39e9631b-5925-4950-ab75-2ab447df5e0e already exists
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 END RequestId: 9cf983a1-292b-4109-9c00-4a8e720fe1cc
2026-04-22T19:42:52.326000+00:00 2026/04/22/[$LATEST]896de0c77931722fd2b05799dc4d7132 REPORT RequestId: 9cf983a1-292b-4109-9c00-4a8e720fe1cc Duration: 224.34 ms Billed Duration: 225 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be START RequestId: 756edb9d-08fb-418c-a223-6490b4f5b0ac Version: $LATEST
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be [INFO] 2026-04-22T19:42:53.279Z 756edb9d-08fb-418c-a223-6490b4f5b0ac Found credentials in environment variables.
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be [INFO] 2026-04-22T19:42:53.441Z 756edb9d-08fb-418c-a223-6490b4f5b0ac setSecret: both stages readable
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be END RequestId: 756edb9d-08fb-418c-a223-6490b4f5b0ac
2026-04-22T19:42:53.446000+00:00 2026/04/22/[$LATEST]20fd5cafe2ec358e4f06758823fd93be REPORT RequestId: 756edb9d-08fb-418c-a223-6490b4f5b0ac Duration: 213.76 ms Billed Duration: 214 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 START RequestId: dca238f6-9391-4b23-8123-095110338c07 Version: $LATEST
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 [INFO] 2026-04-22T19:42:54.468Z dca238f6-9391-4b23-8123-095110338c07 Found credentials in environment variables.
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 [INFO] 2026-04-22T19:42:54.627Z dca238f6-9391-4b23-8123-095110338c07 testSecret: pending value validated
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 END RequestId: dca238f6-9391-4b23-8123-095110338c07
2026-04-22T19:42:54.632000+00:00 2026/04/22/[$LATEST]43ed70aacb018422a47b123d3bb66f62 REPORT RequestId: dca238f6-9391-4b23-8123-095110338c07 Duration: 210.42 ms Billed Duration: 211 ms Memory Size: 256 MB Max Memory Used: 256 MB
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 START RequestId: 84188510-a9d9-48af-9e6d-fe14febe4980 Version: $LATEST
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 [INFO] 2026-04-22T19:42:55.590Z 84188510-a9d9-48af-9e6d-fe14febe4980 Found credentials in environment variables.
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 [INFO] 2026-04-22T19:42:55.741Z 84188510-a9d9-48af-9e6d-fe14febe4980 finishSecret: (no-op)
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 END RequestId: 84188510-a9d9-48af-9e6d-fe14febe4980
2026-04-22T19:42:55.747000+00:00 2026/04/22/[$LATEST]8ff8e570cf641117b6af2b415e8f3444 REPORT RequestId: 84188510-a9d9-48af-9e6d-fe14febe4980 Duration: 202.32 ms Billed Duration: 203 ms Memory Size: 256 MB Max Memory Used: 256 MB/app/handler.py
def finish_secret(client, arn, token):
# TODO: promote the AWSPENDING version to AWSCURRENT here.
# Without this, rotation will appear to succeed from Secrets
# Manager's orchestrator but AWSCURRENT never advances and
# consumers keep reading the old password.
logger.info("finishSecret: (no-op)")def finish_secret(client, arn, token):
desc = client.describe_secret(SecretId=arn)
current_version = None
for version_id, stages in desc["VersionIdsToStages"].items():
if "AWSCURRENT" in stages:
if version_id == token:
logger.info("finishSecret: %s already AWSCURRENT", token)
return
current_version = version_id
break
client.update_secret_version_stage(
SecretId=arn,
VersionStage="AWSCURRENT",
MoveToVersionId=token,
RemoveFromVersionId=current_version,
)
logger.info("finishSecret: promoted %s to AWSCURRENT", token)Capture KMS/secret ARNs
KEY_ID=$(aws kms describe-key --key-id alias/app-rotation-key --query 'KeyMetadata.KeyId' --output text)
KEY_ARN="arn:aws:kms:us-east-1:000000000000:key/${KEY_ID}"
SECRET_ARN=$(aws secretsmanager describe-secret --secret-id app/db/password --query 'ARN' --output text)
echo "KEY_ARN=$KEY_ARN"
echo "SECRET_ARN=$SECRET_ARN"KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/f559f8aa-d6b9-4364-9b56-83935f1be99f SECRET_ARN=arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt [stdout] KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/f559f8aa-d6b9-4364-9b56-83935f1be99f SECRET_ARN=arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt
Update IAM role policy with KMS permissions
cat > /tmp/rotator-policy.json <<'JSON'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SecretAccess",
"Effect": "Allow",
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
],
"Resource": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt"
},
{
"Sid": "GenerateRandomPassword",
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
},
{
"Sid": "UseCmkViaSecretsManager",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/f559f8aa-d6b9-4364-9b56-83935f1be99f",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
}
}
}
]
}
JSON
aws iam put-role-policy \
--role-name secrets-rotator-role \
--policy-name SecretsRotatorPolicy \
--policy-document file:///tmp/rotator-policy.json
echo "role policy updated"role policy updated [stdout] role policy updated
Update KMS key policy
cat > /tmp/key-policy.json <<'JSON'
{
"Version": "2012-10-17",
"Id": "app-rotation-key-policy",
"Statement": [
{
"Sid": "EnableRootAdmin",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::000000000000:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowRotatorRoleUseOfKeyViaSecretsManager",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/secrets-rotator-role"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
}
}
},
{
"Sid": "AllowSecretsManagerUseOfKey",
"Effect": "Allow",
"Principal": {
"Service": "secretsmanager.amazonaws.com"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
JSON
KEY_ID=$(aws kms describe-key --key-id alias/app-rotation-key --query 'KeyMetadata.KeyId' --output text)
aws kms put-key-policy \
--key-id "$KEY_ID" \
--policy-name default \
--policy file:///tmp/key-policy.json
echo "key policy updated"key policy updated [stdout] key policy updated
Add Lambda resource policy for Secrets Manager
SECRET_ARN=$(aws secretsmanager describe-secret --secret-id app/db/password --query 'ARN' --output text) aws lambda add-permission \ --function-name secrets-rotator \ --statement-id SecretsManagerInvoke \ --action lambda:InvokeFunction \ --principal secretsmanager.amazonaws.com \ --source-arn "$SECRET_ARN"
{
"Statement": "{\"Sid\": \"SecretsManagerInvoke\", \"Effect\": \"Allow\", \"Action\": \"lambda:InvokeFunction\", \"Resource\": \"arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator\", \"Principal\": {\"Service\": \"secretsmanager.amazonaws.com\"}, \"Condition\": {\"ArnLike\": {\"AWS:SourceArn\": \"arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt\"}}}"
}
[stdout]
{
"Statement": "{\"Sid\": \"SecretsManagerInvoke\", \"Effect\": \"Allow\", \"Action\": \"lambda:InvokeFunction\", \"Resource\": \"arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator\", \"Principal\": {\"Service\": \"secretsmanager.amazonaws.com\"}, \"Condition\": {\"ArnLike\": {\"AWS:SourceArn\": \"arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt\"}}}"
}Redeploy Lambda
WORKDIR="$(mktemp -d)"
cp /app/handler.py "${WORKDIR}/handler.py"
(cd "$WORKDIR" && zip -q handler.zip handler.py)
aws lambda update-function-code \
--function-name secrets-rotator \
--zip-file "fileb://${WORKDIR}/handler.zip" \
--query 'LastUpdateStatus' --output text
for _ in $(seq 1 30); do
STATUS=$(aws lambda get-function --function-name secrets-rotator --query 'Configuration.LastUpdateStatus' --output text 2>/dev/null || true)
[ "$STATUS" = "Successful" ] && { echo "Lambda ready"; break; }
sleep 1
doneInProgress Lambda ready [stdout] InProgress Lambda ready
Test rotation end-to-end
echo "=== BEFORE ==="
aws secretsmanager describe-secret --secret-id app/db/password --query '{Stages:VersionIdsToStages}' --output json
BEFORE_CURRENT_VALUE=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'SecretString' --output text)
BEFORE_CURRENT_VERSION=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'VersionId' --output text)
echo "BEFORE VersionId=$BEFORE_CURRENT_VERSION"
echo "BEFORE SecretString=$BEFORE_CURRENT_VALUE"
echo
echo "=== ROTATE ==="
aws secretsmanager rotate-secret --secret-id app/db/password
sleep 5
echo
echo "=== AFTER ==="
aws secretsmanager describe-secret --secret-id app/db/password --query '{Stages:VersionIdsToStages}' --output json
AFTER_CURRENT_VALUE=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'SecretString' --output text)
AFTER_CURRENT_VERSION=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'VersionId' --output text)
echo "AFTER VersionId=$AFTER_CURRENT_VERSION"
echo "AFTER SecretString=$AFTER_CURRENT_VALUE"
echo
if [ "$BEFORE_CURRENT_VERSION" != "$AFTER_CURRENT_VERSION" ] && [ "$BEFORE_CURRENT_VALUE" != "$AFTER_CURRENT_VALUE" ]; then
echo "SUCCESS: AWSCURRENT advanced and value changed"
else
echo "FAIL: AWSCURRENT did NOT advance"
fi=== BEFORE ===
{
"Stages": {
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSCURRENT"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING"
]
}
}
BEFORE VersionId=5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783
BEFORE SecretString={"password": "initial-placeholder-value"}
=== ROTATE ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"VersionId": "39e9631b-5925-4950-ab75-2ab447df5e0e"
}
=== AFTER ===
{
"Stages": {
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSPREVIOUS"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING",
"AWSCURRENT"
]
}
}
AFTER VersionId=39e9631b-5925-4950-ab75-2ab447df5e0e
AFTER SecretString={"password": "PKvn7gmxUzKYzWoWXWpnj4Bb7YCYDvF4"}
SUCCESS: AWSCURRENT advanced and value changed
[stdout]
=== BEFORE ===
{
"Stages": {
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSCURRENT"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING"
]
}
}
BEFORE VersionId=5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783
BEFORE SecretString={"password": "initial-placeholder-value"}
=== ROTATE ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"VersionId": "39e9631b-5925-4950-ab75-2ab447df5e0e"
}
=== AFTER ===
{
"Stages": {
"5f6dcd1b-5af5-4fc8-baf6-51e3bb0b9783": [
"AWSPREVIOUS"
],
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPENDING",
"AWSCURRENT"
]
}
}
AFTER VersionId=39e9631b-5925-4950-ab75-2ab447df5e0e
AFTER SecretString={"password": "PKvn7gmxUzKYzWoWXWpnj4Bb7YCYDvF4"}
SUCCESS: AWSCURRENT advanced and value changedRun a second rotation
echo "=== Second rotation ===" BEFORE_VID=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'VersionId' --output text) BEFORE_VAL=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'SecretString' --output text) aws secretsmanager rotate-secret --secret-id app/db/password sleep 5 AFTER_VID=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'VersionId' --output text) AFTER_VAL=$(aws secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSCURRENT --query 'SecretString' --output text) aws secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages' echo "BEFORE_VID=$BEFORE_VID AFTER_VID=$AFTER_VID" [ "$BEFORE_VID" != "$AFTER_VID" ] && [ "$BEFORE_VAL" != "$AFTER_VAL" ] && echo "SUCCESS 2nd rotation" || echo "FAIL"
=== Second rotation ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"VersionId": "d0682ffd-948b-4fbf-8e57-929a85627433"
}
{
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPREVIOUS"
],
"d0682ffd-948b-4fbf-8e57-929a85627433": [
"AWSPENDING",
"AWSCURRENT"
]
}
BEFORE_VID=39e9631b-5925-4950-ab75-2ab447df5e0e AFTER_VID=d0682ffd-948b-4fbf-8e57-929a85627433
SUCCESS 2nd rotation
[stdout]
=== Second rotation ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-zLciVt",
"Name": "app/db/password",
"VersionId": "d0682ffd-948b-4fbf-8e57-929a85627433"
}
{
"39e9631b-5925-4950-ab75-2ab447df5e0e": [
"AWSPREVIOUS"
],
"d0682ffd-948b-4fbf-8e57-929a85627433": [
"AWSPENDING",
"AWSCURRENT"
]
}
BEFORE_VID=39e9631b-5925-4950-ab75-2ab447df5e0e AFTER_VID=d0682ffd-948b-4fbf-8e57-929a85627433
SUCCESS 2nd rotationNo file changes, the agent left the workspace untouched.
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/AjBwednM6VjLUnjkX6UXv/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 8 items
../tests/test_outputs.py::test_aws_cli_binary_is_real PASSED [ 12%]
../tests/test_outputs.py::test_localstack_reachable PASSED [ 25%]
../tests/test_outputs.py::test_secret_exists_and_uses_customer_cmk PASSED [ 37%]
../tests/test_outputs.py::test_rotation_lambda_exists PASSED [ 50%]
../tests/test_outputs.py::test_role_policy_grants_kms_actions PASSED [ 62%]
../tests/test_outputs.py::test_kms_key_policy_grants_lambda_role PASSED [ 75%]
../tests/test_outputs.py::test_lambda_permission_allows_secretsmanager_invoke PASSED [ 87%]
../tests/test_outputs.py::test_rotate_secret_advances_awscurrent PASSED [100%]
=============================== warnings summary ===============================
test_outputs.py: 14 warnings
/root/.cache/uv/archive-v0/AjBwednM6VjLUnjkX6UXv/lib/python3.12/site-packages/botocore/auth.py:424: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime_now = datetime.datetime.utcnow()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_outputs.py::test_aws_cli_binary_is_real
PASSED ../tests/test_outputs.py::test_localstack_reachable
PASSED ../tests/test_outputs.py::test_secret_exists_and_uses_customer_cmk
PASSED ../tests/test_outputs.py::test_rotation_lambda_exists
PASSED ../tests/test_outputs.py::test_role_policy_grants_kms_actions
PASSED ../tests/test_outputs.py::test_kms_key_policy_grants_lambda_role
PASSED ../tests/test_outputs.py::test_lambda_permission_allows_secretsmanager_invoke
PASSED ../tests/test_outputs.py::test_rotate_secret_advances_awscurrent
======================== 8 passed, 14 warnings in 5.04s ========================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_333bc09ac60e4809. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_333bc09ac60e4809 · verifier authoritative; classifier explanatory.