tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.List files in /app directory
ls -la /app/
total 28 drwxr-xr-x 1 root root 4096 Apr 22 19:10 . drwxr-xr-x 1 root root 4096 Apr 23 05:36 .. -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 23 05:36 .. -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
Describe the secret
aws secretsmanager describe-secret --secret-id app/db/password
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-vqTQFV",
"Name": "app/db/password",
"Description": "App DB password, rotated by Lambda",
"KmsKeyId": "arn:aws:kms:us-east-1:000000000000:key/b33eb077-636f-4783-8ed0-4f04eff43809",
"RotationEnabled": true,
"RotationLambdaARN": "arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator",
"RotationRules": {
"AutomaticallyAfterDays": 30
},
"LastRotatedDate": "2026-04-23T05:36:25+00:00",
"LastChangedDate": "2026-04-23T05:36:21.865000+00:00",
"LastAccessedDate": "2026-04-23T00:00:00+00:00",
"NextRotationDate": "2026-05-23T05:36:20+00:00",
"VersionIdsToStages": {
"4c8f5613-6189-4001-b747-7b7db64bbe9d": [
"AWSCURRENT"
],
"584dea21-2260-49f4-8ee9-cdde89f985f2": [
"AWSPENDING"
]
},
"CreatedDate": "2026-04-23T05:36:13.533154+00:00"
}
[stdout]
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-vqTQFV",
"Name": "app/db/password",
"Description": "App DB password, rotated by Lambda",
"KmsKeyId": "arn:aws:kms:us-east-1:000000000000:key/b33eb077-636f-4783-8ed0-4f04eff43809",
"RotationEnabled": true,
"RotationLambdaARN": "arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator",
"RotationRules": {
"AutomaticallyAfterDays": 30
},
"LastRotatedDate": "2026-04-23T05:36:25+00:00",
"LastChangedDate": "2026-04-23T05:36:21.865000+00:00",
"LastAccessedDate": "2026-04-23T00:00:00+00:00",
"NextRotationDate": "2026-05-23T05:36:20+00:00",
"VersionIdsToStages": {
"4c8f5613-6189-4001-b747-7b7db64bbe9d": [
"AWSCURRENT"
],
"584dea21-2260-49f4-8ee9-cdde89f985f2": [
"AWSPENDING"
]
},
"CreatedDate": "2026-04-23T05:36:13.533154+00:00"
}Check lambda policy and IAM role
aws lambda get-policy --function-name secrets-rotator 2>&1 | head -30; echo "---"; aws iam get-role-policy --role-name secrets-rotator-role --policy-name SecretsRotatorPolicy
aws: [ERROR]: An error occurred (ResourceNotFoundException) when calling the GetPolicy operation: The resource you requested does not exist.
Additional error details:
Type: User
---
{
"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-vqTQFV"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
}
]
}
}
[stdout]
aws: [ERROR]: An error occurred (ResourceNotFoundException) when calling the GetPolicy operation: The resource you requested does not exist.
Additional error details:
Type: User
---
{
"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-vqTQFV"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
}
]
}
}Check KMS key policy and grants
aws kms get-key-policy --key-id alias/app-rotation-key --policy-name default; echo "---grants---"; aws kms list-grants --key-id alias/app-rotation-key
{
"Policy": "{\n \"Version\": \"2012-10-17\",\n \"Id\": \"app-rotation-key-policy\",\n \"Statement\": [\n {\n \"Sid\": \"EnableRootAdmin\",\n \"Effect\": \"Allow\",\n \"Principal\": { \"AWS\": \"arn:aws:iam::000000000000:root\" },\n \"Action\": \"kms:*\",\n \"Resource\": \"*\"\n }\n ]\n}"
}
---grants---
{
"Grants": []
}
[stdout]
{
"Policy": "{\n \"Version\": \"2012-10-17\",\n \"Id\": \"app-rotation-key-policy\",\n \"Statement\": [\n {\n \"Sid\": \"EnableRootAdmin\",\n \"Effect\": \"Allow\",\n \"Principal\": { \"AWS\": \"arn:aws:iam::000000000000:root\" },\n \"Action\": \"kms:*\",\n \"Resource\": \"*\"\n }\n ]\n}"
}
---grants---
{
"Grants": []
}/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):
metadata = client.describe_secret(SecretId=arn)
current_version = None
for version, stages in metadata["VersionIdsToStages"].items():
if "AWSCURRENT" in stages:
if version == token:
logger.info("finishSecret: version %s already AWSCURRENT", token)
return
current_version = version
break
client.update_secret_version_stage(
SecretId=arn,
VersionStage="AWSCURRENT",
MoveToVersionId=token,
RemoveFromVersionId=current_version,
)
logger.info("finishSecret: promoted %s to AWSCURRENT", token)Repackage and update Lambda code
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 textInProgress [stdout] InProgress
Wait for Lambda update to complete
for _ in $(seq 1 30); do STATE=$(aws lambda get-function --function-name secrets-rotator --query 'Configuration.LastUpdateStatus' --output text); [ "$STATE" = "Successful" ] && break; sleep 1; done; echo "Update status: $STATE"
Update status: Successful [stdout] Update status: Successful
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 AllowSecretsManagerInvoke \ --action lambda:InvokeFunction \ --principal secretsmanager.amazonaws.com \ --source-arn "$SECRET_ARN"
{
"Statement": "{\"Sid\": \"AllowSecretsManagerInvoke\", \"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-vqTQFV\"}}}"
}
[stdout]
{
"Statement": "{\"Sid\": \"AllowSecretsManagerInvoke\", \"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-vqTQFV\"}}}"
}Add KMS permissions to the role's inline policy
SECRET_ARN=$(aws secretsmanager describe-secret --secret-id app/db/password --query 'ARN' --output text)
KEY_ARN=$(aws kms describe-key --key-id alias/app-rotation-key --query 'KeyMetadata.Arn' --output text)
cat > /tmp/inline-policy.json <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
],
"Resource": "${SECRET_ARN}"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "${KEY_ARN}",
"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/inline-policy.json
echo "policy updated"policy updated [stdout] policy updated
Trigger rotation and inspect result
echo "=== BEFORE ==="; aws secretsmanager get-secret-value --secret-id app/db/password --query '{VersionId:VersionId,SecretString:SecretString}'
echo
echo "=== rotate-secret ==="
aws secretsmanager rotate-secret --secret-id app/db/password
echo
sleep 3
echo "=== AFTER ==="
aws secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages'
aws secretsmanager get-secret-value --secret-id app/db/password --query '{VersionId:VersionId,SecretString:SecretString}'=== BEFORE ===
{
"VersionId": "4c8f5613-6189-4001-b747-7b7db64bbe9d",
"SecretString": "{\"password\": \"initial-placeholder-value\"}"
}
=== rotate-secret ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-vqTQFV",
"Name": "app/db/password",
"VersionId": "584dea21-2260-49f4-8ee9-cdde89f985f2"
}
=== AFTER ===
{
"4c8f5613-6189-4001-b747-7b7db64bbe9d": [
"AWSPREVIOUS"
],
"584dea21-2260-49f4-8ee9-cdde89f985f2": [
"AWSPENDING",
"AWSCURRENT"
]
}
{
"VersionId": "584dea21-2260-49f4-8ee9-cdde89f985f2",
"SecretString": "{\"password\": \"hstyhuXZoon6l2pp3vD65eQjVkeCZvI9\"}"
}
[stdout]
=== BEFORE ===
{
"VersionId": "4c8f5613-6189-4001-b747-7b7db64bbe9d",
"SecretString": "{\"password\": \"initial-placeholder-value\"}"
}
=== rotate-secret ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-vqTQFV",
"Name": "app/db/password",
"VersionId": "584dea21-2260-49f4-8ee9-cdde89f985f2"
}
=== AFTER ===
{
"4c8f5613-6189-4001-b747-7b7db64bbe9d": [
"AWSPREVIOUS"
],
"584dea21-2260-49f4-8ee9-cdde89f985f2": [
"AWSPENDING",
"AWSCURRENT"
]
}
{
"VersionId": "584dea21-2260-49f4-8ee9-cdde89f985f2",
"SecretString": "{\"password\": \"hstyhuXZoon6l2pp3vD65eQjVkeCZvI9\"}"
}Second rotation test
echo "=== SECOND ROTATION ==="
aws secretsmanager rotate-secret --secret-id app/db/password
sleep 3
aws secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages'
aws secretsmanager get-secret-value --secret-id app/db/password --query '{VersionId:VersionId,SecretString:SecretString}'=== SECOND ROTATION ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-vqTQFV",
"Name": "app/db/password",
"VersionId": "84912c5f-4983-48e8-be9d-913cae973c51"
}
{
"584dea21-2260-49f4-8ee9-cdde89f985f2": [
"AWSPREVIOUS"
],
"84912c5f-4983-48e8-be9d-913cae973c51": [
"AWSPENDING",
"AWSCURRENT"
]
}
{
"VersionId": "84912c5f-4983-48e8-be9d-913cae973c51",
"SecretString": "{\"password\": \"GSvAFwoOOhqPXGHrstdzYmUTapg5xwU8\"}"
}
[stdout]
=== SECOND ROTATION ===
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-vqTQFV",
"Name": "app/db/password",
"VersionId": "84912c5f-4983-48e8-be9d-913cae973c51"
}
{
"584dea21-2260-49f4-8ee9-cdde89f985f2": [
"AWSPREVIOUS"
],
"84912c5f-4983-48e8-be9d-913cae973c51": [
"AWSPENDING",
"AWSCURRENT"
]
}
{
"VersionId": "84912c5f-4983-48e8-be9d-913cae973c51",
"SecretString": "{\"password\": \"GSvAFwoOOhqPXGHrstdzYmUTapg5xwU8\"}"
}No 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/A4vyHVsuIjTl4bXahx1Ex/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 FAILED [ 75%]
../tests/test_outputs.py::test_lambda_permission_allows_secretsmanager_invoke PASSED [ 87%]
../tests/test_outputs.py::test_rotate_secret_advances_awscurrent PASSED [100%]
=================================== FAILURES ===================================
____________________ test_kms_key_policy_grants_lambda_role ____________________
iam = <botocore.client.IAM object at 0xffff83e8c950>
kms = <botocore.client.KMS object at 0xffff847db7a0>
def test_kms_key_policy_grants_lambda_role(iam, kms):
role_arn = iam.get_role(RoleName=ROLE_NAME)["Role"]["Arn"]
policy_str = kms.get_key_policy(KeyId=KEY_ALIAS, PolicyName="default")["Policy"]
policy = json.loads(policy_str)
match = False
for st in policy.get("Statement", []):
if _statement_matches(
st,
principal_arn=role_arn,
required_actions=REQUIRED_KMS_ACTIONS,
):
match = True
break
> assert match, (
f"KMS key policy on {KEY_ALIAS} has no Allow statement granting "
f"{sorted(REQUIRED_KMS_ACTIONS)} to principal {role_arn}. Key "
f"policy: {policy_str}"
)
E AssertionError: KMS key policy on alias/app-rotation-key has no Allow statement granting ['kms:Decrypt', 'kms:GenerateDataKey'] to principal arn:aws:iam::000000000000:role/secrets-rotator-role. Key policy: {
E "Version": "2012-10-17",
E "Id": "app-rotation-key-policy",
E "Statement": [
E {
E "Sid": "EnableRootAdmin",
E "Effect": "Allow",
E "Principal": { "AWS": "arn:aws:iam::000000000000:root" },
E "Action": "kms:*",
E "Resource": "*"
E }
E ]
E }
E assert False
/tests/test_outputs.py:256: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 14 warnings
/root/.cache/uv/archive-v0/A4vyHVsuIjTl4bXahx1Ex/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_lambda_permission_allows_secretsmanager_invoke
PASSED ../tests/test_outputs.py::test_rotate_secret_advances_awscurrent
FAILED ../tests/test_outputs.py::test_kms_key_policy_grants_lambda_role - Ass...
=================== 1 failed, 7 passed, 14 warnings in 5.47s ===================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_e6ef7b5814bd4eb5. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_e6ef7b5814bd4eb5 · verifier authoritative; classifier explanatory.