tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Check environment tools
echo "AWS_ENDPOINT: ${AWS_ENDPOINT_URL:-not set}"; aws --version; python3 --version; which jq zip; ls /app 2>/dev/null || echo "no /app"AWS_ENDPOINT: http://localstack:4566 aws-cli/2.34.39 Python/3.14.4 Linux/6.1.158 exe/x86_64.ubuntu.24 Python 3.12.3 /usr/bin/jq /usr/bin/zip [stdout] AWS_ENDPOINT: http://localstack:4566 aws-cli/2.34.39 Python/3.14.4 Linux/6.1.158 exe/x86_64.ubuntu.24 Python 3.12.3 /usr/bin/jq /usr/bin/zip
Verify localstack connectivity
aws sts get-caller-identity 2>&1 | head -20; echo "---"; aws s3api list-buckets 2>&1 | head -10
{
"UserId": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
{
"Buckets": [],
"Owner": {
"ID": "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a"
},
"Prefix": null
}
[stdout]
{
"UserId": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
{
"Buckets": [],
"Owner": {
"ID": "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a"
},
"Prefix": null
}Create build dir
mkdir -p /app/build && ls /app
build [stdout] build
Create S3 bucket
aws s3api create-bucket --bucket worker-bucket 2>&1 | head -20
{
"Location": "/worker-bucket",
"BucketArn": "arn:aws:s3:::worker-bucket"
}
[stdout]
{
"Location": "/worker-bucket",
"BucketArn": "arn:aws:s3:::worker-bucket"
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Create WorkerExecRole
aws iam create-role --role-name WorkerExecRole --assume-role-policy-document file:///app/build/exec-trust.json 2>&1 | head -30
{
"Role": {
"Path": "/",
"RoleName": "WorkerExecRole",
"RoleId": "AROAQAAAAAAAJ2OXVZDJW",
"Arn": "arn:aws:iam::000000000000:role/WorkerExecRole",
"CreateDate": "2026-04-29T19:49:56.907527+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
}
}
[stdout]
{
"Role": {
"Path": "/",
"RoleName": "WorkerExecRole",
"RoleId": "AROAQAAAAAAAJ2OXVZDJW",
"Arn": "arn:aws:iam::000000000000:role/WorkerExecRole",
"CreateDate": "2026-04-29T19:49:56.907527+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
}
}Create WorkerRole with trust to WorkerExecRole
EXEC_ARN="arn:aws:iam::000000000000:role/WorkerExecRole"; cat > /app/build/worker-trust.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "${EXEC_ARN}" },
"Action": "sts:AssumeRole"
}
]
}
EOF
aws iam create-role --role-name WorkerRole --assume-role-policy-document file:///app/build/worker-trust.json 2>&1 | head -30{
"Role": {
"Path": "/",
"RoleName": "WorkerRole",
"RoleId": "AROAQAAAAAAANETFF6KHF",
"Arn": "arn:aws:iam::000000000000:role/WorkerRole",
"CreateDate": "2026-04-29T19:50:01.672115+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/WorkerExecRole"
},
"Action": "sts:AssumeRole"
}
]
}
}
}
[stdout]
{
"Role": {
"Path": "/",
"RoleName": "WorkerRole",
"RoleId": "AROAQAAAAAAANETFF6KHF",
"Arn": "arn:aws:iam::000000000000:role/WorkerRole",
"CreateDate": "2026-04-29T19:50:01.672115+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/WorkerExecRole"
},
"Action": "sts:AssumeRole"
}
]
}
}
}Attach inline policies to roles
WORKER_ARN="arn:aws:iam::000000000000:role/WorkerRole"
cat > /app/build/exec-inline.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "${WORKER_ARN}"
}
]
}
EOF
aws iam put-role-policy --role-name WorkerExecRole --policy-name ExecInline --policy-document file:///app/build/exec-inline.json 2>&1
echo "---"
cat > /app/build/worker-inline.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject","s3:GetObject"],
"Resource": "arn:aws:s3:::worker-bucket/*"
}
]
}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name WorkerS3 --policy-document file:///app/build/worker-inline.json 2>&1--- [stdout] ---
import json
import os
import boto3
WORKER_ROLE_ARN = "arn:aws:iam::000000000000:role/WorkerRole"
BUCKET = "worker-bucket"
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localstack:4566")
def handler(event, context):
sts = boto3.client("sts", endpoint_url=ENDPOINT)
assumed = sts.assume_role(
RoleArn=WORKER_ROLE_ARN,
RoleSessionName="worker-session",
)
creds = assumed["Credentials"]
expiration = creds["Expiration"].isoformat()
s3 = boto3.client(
"s3",
endpoint_url=ENDPOINT,
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
s3.put_object(Bucket=BUCKET, Key="worker/run.txt", Body=b"hello from worker")
sts_assumed = boto3.client(
"sts",
endpoint_url=ENDPOINT,
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
ident = sts_assumed.get_caller_identity()
return {
"statusCode": 200,
"body": json.dumps(
{
"expiration": expiration,
"assumedArn": ident["Arn"],
"assumedAccount": ident["Account"],
}
),
}
Zip the lambda
cd /app/build && zip -q worker.zip worker.py && ls -la worker.zip
-rw-r--r-- 1 root root 710 Apr 29 19:50 worker.zip [stdout] -rw-r--r-- 1 root root 710 Apr 29 19:50 worker.zip
{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Create the worker lambda
aws lambda create-function \
--function-name worker \
--runtime python3.11 \
--role arn:aws:iam::000000000000:role/WorkerExecRole \
--handler worker.handler \
--zip-file fileb:///app/build/worker.zip \
--environment 'Variables={AWS_ENDPOINT_URL=http://localstack:4566}' \
--timeout 30 2>&1 | head -30{
"FunctionName": "worker",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:worker",
"Runtime": "python3.11",
"Role": "arn:aws:iam::000000000000:role/WorkerExecRole",
"Handler": "worker.handler",
"CodeSize": 710,
"Description": "",
"Timeout": 30,
"MemorySize": 128,
"LastModified": "2026-04-29T19:50:37.177237+0000",
"CodeSha256": "FDO2w634cjY/GIAe5w8AT9+kSGXZmxeTJBPo9Hy/5ow=",
"Version": "$LATEST",
"Environment": {
"Variables": {
"AWS_ENDPOINT_URL": "http://localstack:4566"
}
},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "4c5d4036-2e35-47eb-b06b-d9b2b258547f",
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
[stdout]
{
"FunctionName": "worker",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:worker",
"Runtime": "python3.11",
"Role": "arn:aws:iam::000000000000:role/WorkerExecRole",
"Handler": "worker.handler",
"CodeSize": 710,
"Description": "",
"Timeout": 30,
"MemorySize": 128,
"LastModified": "2026-04-29T19:50:37.177237+0000",
"CodeSha256": "FDO2w634cjY/GIAe5w8AT9+kSGXZmxeTJBPo9Hy/5ow=",
"Version": "$LATEST",
"Environment": {
"Variables": {
"AWS_ENDPOINT_URL": "http://localstack:4566"
}
},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "4c5d4036-2e35-47eb-b06b-d9b2b258547f",
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {Wait for lambda to become active
for i in 1 2 3 4 5 6 7 8 9 10; do STATE=$(aws lambda get-function --function-name worker --query 'Configuration.State' --output text 2>/dev/null) echo "state: $STATE" if [ "$STATE" = "Active" ]; then break; fi sleep 1 done
state: Pending state: Pending state: Active [stdout] state: Pending state: Pending state: Active
Compute cutoff ISO and write revoke policy JSON
CUTOFF=$(python3 -c "from datetime import datetime, timezone; print(datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.') + f'{datetime.now(timezone.utc).microsecond // 1000:03d}' + 'Z')")
echo "$CUTOFF" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$' && echo "regex OK"
echo "$CUTOFF" > /app/build/cutoff.txt
cat > /app/build/revoke.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "${CUTOFF}"
}
}
}
]
}
EOF
cat /app/build/revoke.json2026-04-29T19:50:54.597Z
regex OK
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T19:50:54.597Z"
}
}
}
]
}
[stdout]
2026-04-29T19:50:54.597Z
regex OK
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T19:50:54.597Z"
}
}
}
]
}Attach AWSRevokeOlderSessions inline policy
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke.json 2>&1 echo "---" aws iam get-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions 2>&1
---
{
"RoleName": "WorkerRole",
"PolicyName": "AWSRevokeOlderSessions",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T19:50:54.597Z"
}
}
}
]
}
}
[stdout]
---
{
"RoleName": "WorkerRole",
"PolicyName": "AWSRevokeOlderSessions",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T19:50:54.597Z"
}
}
}
]
}
}Write SSM pointers
CUTOFF=$(cat /app/build/cutoff.txt) WORKER_ARN="arn:aws:iam::000000000000:role/WorkerRole" EXEC_ARN="arn:aws:iam::000000000000:role/WorkerExecRole" LAMBDA_ARN="arn:aws:lambda:us-east-1:000000000000:function:worker" aws ssm put-parameter --name /harbor/revoke/role-name --type String --value WorkerRole --overwrite >/dev/null aws ssm put-parameter --name /harbor/revoke/role-arn --type String --value "$WORKER_ARN" --overwrite >/dev/null aws ssm put-parameter --name /harbor/revoke/exec-role-arn --type String --value "$EXEC_ARN" --overwrite >/dev/null aws ssm put-parameter --name /harbor/revoke/bucket --type String --value worker-bucket --overwrite >/dev/null aws ssm put-parameter --name /harbor/revoke/lambda-arn --type String --value "$LAMBDA_ARN" --overwrite >/dev/null aws ssm put-parameter --name /harbor/revoke/cutoff-iso --type String --value "$CUTOFF" --overwrite >/dev/null echo "ssm written" aws ssm get-parameters-by-path --path /harbor/revoke --query 'Parameters[].[Name,Value]' --output text
ssm written /harbor/revoke/role-name WorkerRole /harbor/revoke/role-arn arn:aws:iam::000000000000:role/WorkerRole /harbor/revoke/exec-role-arn arn:aws:iam::000000000000:role/WorkerExecRole /harbor/revoke/bucket worker-bucket /harbor/revoke/lambda-arn arn:aws:lambda:us-east-1:000000000000:function:worker /harbor/revoke/cutoff-iso 2026-04-29T19:50:54.597Z [stdout] ssm written /harbor/revoke/role-name WorkerRole /harbor/revoke/role-arn arn:aws:iam::000000000000:role/WorkerRole /harbor/revoke/exec-role-arn arn:aws:iam::000000000000:role/WorkerExecRole /harbor/revoke/bucket worker-bucket /harbor/revoke/lambda-arn arn:aws:lambda:us-east-1:000000000000:function:worker /harbor/revoke/cutoff-iso 2026-04-29T19:50:54.597Z
Invoke worker lambda and verify S3 write
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out.json 2>&1
echo "---response body---"
cat /app/build/out.json
echo
echo "---s3 object check---"
aws s3api head-object --bucket worker-bucket --key worker/run.txt 2>&1 | head -10{
"StatusCode": 200,
"FunctionError": "Unhandled",
"ExecutedVersion": "$LATEST"
}
---response body---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "30487c7d-f79a-4f34-8fa4-f5b33dc62290", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
---s3 object check---
aws: [ERROR]: An error occurred (404) when calling the HeadObject operation: Not Found
[stdout]
{
"StatusCode": 200,
"FunctionError": "Unhandled",
"ExecutedVersion": "$LATEST"
}
---response body---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "30487c7d-f79a-4f34-8fa4-f5b33dc62290", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
---s3 object check---
aws: [ERROR]: An error occurred (404) when calling the HeadObject operation: Not FoundSanity check direct S3 write works
echo test | aws s3 cp - s3://worker-bucket/test-direct.txt 2>&1 aws s3 ls s3://worker-bucket/ 2>&1
2026-04-29 19:51:38 5 test-direct.txt [stdout] 2026-04-29 19:51:38 5 test-direct.txt
Check lambda logs
aws logs describe-log-groups --log-group-name-prefix /aws/lambda/worker 2>&1 | head -10 echo "---" LG=$(aws logs describe-log-streams --log-group-name /aws/lambda/worker --order-by LastEventTime --descending --max-items 1 --query 'logStreams[0].logStreamName' --output text 2>/dev/null) echo "stream: $LG" aws logs get-log-events --log-group-name /aws/lambda/worker --log-stream-name "$LG" --query 'events[].message' --output text 2>&1 | head -50
{
"logGroups": [
{
"logGroupName": "/aws/lambda/worker",
"creationTime": 1777492285656,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/worker:*",
"storedBytes": 961,
"logGroupClass": "STANDARD",
"logGroupArn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/worker"
---
stream: 2026/04/29/[$LATEST]d94fef47a1aaec333b9b98a3567f9349
START RequestId: 30487c7d-f79a-4f34-8fa4-f5b33dc62290 Version: $LATEST
[ERROR] ClientError: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Traceback (most recent call last):
File "/var/task/worker.py", line 26, in handler
s3.put_object(Bucket=BUCKET, Key="worker/run.txt", Body=b"hello from worker")
File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 602, in _api_call
return self._make_api_call(operation_name, kwargs)
File "/var/lang/lib/python3.11/site-packages/botocore/context.py", line 123, in wrapper
return func(*args, **kwargs)
File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 1078, in _make_api_call
raise error_class(parsed_response, operation_name)
END RequestId: 30487c7d-f79a-4f34-8fa4-f5b33dc62290
REPORT RequestId: 30487c7d-f79a-4f34-8fa4-f5b33dc62290 Duration: 257.95 ms Billed Duration: 258 ms Memory Size: 128 MB Max Memory Used: 128 MB
[stdout]
{
"logGroups": [
{
"logGroupName": "/aws/lambda/worker",
"creationTime": 1777492285656,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/worker:*",
"storedBytes": 961,
"logGroupClass": "STANDARD",
"logGroupArn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/worker"
---
stream: 2026/04/29/[$LATEST]d94fef47a1aaec333b9b98a3567f9349
START RequestId: 30487c7d-f79a-4f34-8fa4-f5b33dc62290 Version: $LATEST
[ERROR] ClientError: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Traceback (most recent call last):
File "/var/task/worker.py", line 26, in handler
s3.put_object(Bucket=BUCKET, Key="worker/run.txt", Body=b"hello from worker")
File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 602, in _api_call
return self._make_api_call(operation_name, kwargs)
File "/var/lang/lib/python3.11/site-packages/botocore/context.py", line 123, in wrapper
return func(*args, **kwargs)
File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 1078, in _make_api_call
raise error_class(parsed_response, operation_name)
END RequestId: 30487c7d-f79a-4f34-8fa4-f5b33dc62290
REPORT RequestId: 30487c7d-f79a-4f34-8fa4-f5b33dc62290 Duration: 257.95 ms Billed Duration: 258 ms Memory Size: 128 MB Max Memory Used: 128 MBCheck localstack health/edition
curl -s http://localstack:4566/_localstack/health 2>&1 | head -50
{"features": {"persistence": "disabled"}, "services": {"acm": "disabled", "apigateway": "disabled", "cloudformation": "disabled", "cloudwatch": "disabled", "config": "disabled", "dynamodb": "disabled", "dynamodbstreams": "disabled", "ec2": "disabled", "es": "disabled", "events": "disabled", "firehose": "disabled", "iam": "running", "kinesis": "disabled", "kms": "available", "lambda": "running", "logs": "running", "opensearch": "disabled", "redshift": "disabled", "resource-groups": "disabled", "resourcegroupstaggingapi": "disabled", "route53": "disabled", "route53resolver": "disabled", "s3": "running", "s3control": "disabled", "scheduler": "disabled", "secretsmanager": "available", "ses": "disabled", "sns": "disabled", "sqs": "disabled", "ssm": "running", "stepfunctions": "disabled", "sts": "running", "support": "disabled", "swf": "disabled", "transcribe": "disabled", "account": "disabled", "acm-pca": "disabled", "amplify": "disabled", "apigatewaymanagementapi": "disabled", "apigatewayv2": "disabled", "appconfig": "disabled", "appconfigdata": "disabled", "application-autoscaling": "disabled", "appsync": "disabled", "athena": "disabled", "autoscaling": "disabled", "backup": "disabled", "batch": "disabled", "bedrock-runtime": "disabled", "bedrock": "disabled", "ce": "disabled", "cloudcontrol": "disabled", "cloudfront": "disabled", "cloudtrail": "disabled", "codeartifact": "disabled", "codebuild": "disabled", "codecommit": "disabled", "codeconnections": "disabled", "codedeploy": "disabled", "codepipeline": "disabled", "codestar-connections": "disabled", "cognito-identity": "disabled", "cognito-idp": "disabled", "dms": "disabled", "docdb": "disabled", "ecr": "disabled", "ecs": "disabled", "efs": "disabled", "eks-auth": "disabled", "eks": "disabled", "elasticache": "disabled", "elasticbeanstalk": "disabled", "elb": "disabled", "elbv2": "disabled", "emr-serverless": "disabled", "emr": "disabled", "fis": "disabled", "glacier": "disabled", "glue": "disabled", "identitystore": "disabled", "iot-data": "disabled", "iot": "disabled", "iotwireless": "disabled", "kafka": "disabled", "kinesisanalyticsv2": "disabled", "lakeformation": "disabled", "managedblockchain": "disabled", "mediaconvert": "disabled", "memorydb": "disabled", "mq": "disabled", "mwaa": "disabled", "neptune": "disabled", "organizations": "disabled", "pinpoint": "disabled", "pipes": "disabled", "ram": "disabled", "rds-data": "disabled", "rds": "disabled", "redshift-data": "disabled", "s3tables": "disabled", "sagemaker-runtime": "disabled", "sagemaker": "disabled", "serverlessrepo": "disabled", "servicediscovery": "disabled", "sesv2": "disabled", "shield": "disabled", "sso-admin": "disabled", "textract": "disabled", "timestream-query": "disabled", "timestream-write": "disabled", "transfer": "disabled", "verifiedpermissions": "disabled", "wafv2": "disabled", "xray": "disabled"}, "edition": "pro", "version": "2026.3.0"}
[stdout]
{"features": {"persistence": "disabled"}, "services": {"acm": "disabled", "apigateway": "disabled", "cloudformation": "disabled", "cloudwatch": "disabled", "config": "disabled", "dynamodb": "disabled", "dynamodbstreams": "disabled", "ec2": "disabled", "es": "disabled", "events": "disabled", "firehose": "disabled", "iam": "running", "kinesis": "disabled", "kms": "available", "lambda": "running", "logs": "running", "opensearch": "disabled", "redshift": "disabled", "resource-groups": "disabled", "resourcegroupstaggingapi": "disabled", "route53": "disabled", "route53resolver": "disabled", "s3": "running", "s3control": "disabled", "scheduler": "disabled", "secretsmanager": "available", "ses": "disabled", "sns": "disabled", "sqs": "disabled", "ssm": "running", "stepfunctions": "disabled", "sts": "running", "support": "disabled", "swf": "disabled", "transcribe": "disabled", "account": "disabled", "acm-pca": "disabled", "amplify": "disabled", "apigatewaymanagementapi": "disabled", "apigatewayv2": "disabled", "appconfig": "disabled", "appconfigdata": "disabled", "application-autoscaling": "disabled", "appsync": "disabled", "athena": "disabled", "autoscaling": "disabled", "backup": "disabled", "batch": "disabled", "bedrock-runtime": "disabled", "bedrock": "disabled", "ce": "disabled", "cloudcontrol": "disabled", "cloudfront": "disabled", "cloudtrail": "disabled", "codeartifact": "disabled", "codebuild": "disabled", "codecommit": "disabled", "codeconnections": "disabled", "codedeploy": "disabled", "codepipeline": "disabled", "codestar-connections": "disabled", "cognito-identity": "disabled", "cognito-idp": "disabled", "dms": "disabled", "docdb": "disabled", "ecr": "disabled", "ecs": "disabled", "efs": "disabled", "eks-auth": "disabled", "eks": "disabled", "elasticache": "disabled", "elasticbeanstalk": "disabled", "elb": "disabled", "elbv2": "disabled", "emr-serverless": "disabled", "emr": "disabled", "fis": "disabled", "glacier": "disabled", "glue": "disabled", "identitystore": "disabled", "iot-data": "disabled", "iot": "disabled", "iotwireless": "disabled", "kafka": "disabled", "kinesisanalyticsv2": "disabled", "lakeformation": "disabled", "managedblockchain": "disabled", "mediaconvert": "disabled", "memorydb": "disabled", "mq": "disabled", "mwaa": "disabled", "neptune": "disabled", "organizations": "disabled", "pinpoint": "disabled", "pipes": "disabled", "ram": "disabled", "rds-data": "disabled", "rds": "disabled", "redshift-data": "disabled", "s3tables": "disabled", "sagemaker-runtime": "disabled", "sagemaker": "disabled", "serverlessrepo": "disabled", "servicediscovery": "disabled", "sesv2": "disabled", "shield": "disabled", "sso-admin": "disabled", "textract": "disabled", "timestream-query": "disabled", "timestream-write": "disabled", "transfer": "disabled", "verifiedpermissions": "disabled", "wafv2": "disabled", "xray": "disabled"}, "edition": "pro", "version": "2026.3.0"}Test invoke without revoke policy to isolate
aws iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions 2>&1
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out2.json 2>&1 | head -5
echo "---response without revoke---"
cat /app/build/out2.json
echo{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
---response without revoke---
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:52:04.078937+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
---response without revoke---
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:52:04.078937+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}Test with cutoff in the past
# Test: attach revoke with cutoff far in the past , should allow
cat > /app/build/revoke-old.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"DateLessThan": {"aws:TokenIssueTime": "2020-01-01T00:00:00.000Z"}}
}]
}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke-old.json
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out3.json 2>&1 | head -3
echo "---past cutoff---"
cat /app/build/out3.json; echo{
"StatusCode": 200,
"FunctionError": "Unhandled",
---past cutoff---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "3b88a830-2f32-487f-a66e-811fd6a580e4", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"StatusCode": 200,
"FunctionError": "Unhandled",
---past cutoff---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "3b88a830-2f32-487f-a66e-811fd6a580e4", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Compare with and without revoke
aws iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out4.json 2>&1 | head -3
echo "---no revoke---"; cat /app/build/out4.json; echo
# Now reattach with strict (past) cutoff
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke-old.json
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out5.json 2>&1 | head -3
echo "---revoke past cutoff---"; cat /app/build/out5.json; echo{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
---no revoke---
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:52:49.084570+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
---revoke past cutoff---
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:52:51.320454+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
---no revoke---
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:52:49.084570+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
---revoke past cutoff---
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:52:51.320454+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}Test with cutoff 30 min ago
date -u
# Set cutoff to 30 minutes ago (well within last hour, but well before any role creation)
PAST=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(minutes=30); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}' + 'Z')")
echo "past-cutoff: $PAST"
cat > /app/build/revoke-past30.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{"Effect": "Deny","Action": "*","Resource": "*","Condition": {"DateLessThan": {"aws:TokenIssueTime": "${PAST}"}}}]
}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke-past30.json
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out6.json 2>&1 | head -3
echo "---revoke 30min past---"; cat /app/build/out6.json; echoWed Apr 29 19:53:15 UTC 2026
past-cutoff: 2026-04-29T19:23:15.294Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
---revoke 30min past---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "ffdfa090-7a8a-4881-b3ca-e6bd6db9af36", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
Wed Apr 29 19:53:15 UTC 2026
past-cutoff: 2026-04-29T19:23:15.294Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
---revoke 30min past---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "ffdfa090-7a8a-4881-b3ca-e6bd6db9af36", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Inspect LocalStack config
curl -s http://localstack:4566/_localstack/config 2>&1 | head -100
(Bash completed with no output)
Diagnose LocalStack
curl -sv http://localstack:4566/_localstack/diagnose 2>&1 | head -100
* Host localstack:4566 was resolved. * IPv6: (none) * IPv4: 172.18.0.2 * Trying 172.18.0.2:4566... * Connected to localstack (172.18.0.2) port 4566 > GET /_localstack/diagnose HTTP/1.1 > Host: localstack:4566 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 404 NOT FOUND < Server: TwistedWeb/25.5.0 < Date: Wed, 29 Apr 2026 19:53:47 GMT < Content-Type: text/plain; charset=utf-8 < x-localstack: true < Content-Length: 0 < * Connection #0 to host localstack left intact [stdout] * Host localstack:4566 was resolved. * IPv6: (none) * IPv4: 172.18.0.2 * Trying 172.18.0.2:4566... * Connected to localstack (172.18.0.2) port 4566 > GET /_localstack/diagnose HTTP/1.1 > Host: localstack:4566 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 404 NOT FOUND < Server: TwistedWeb/25.5.0 < Date: Wed, 29 Apr 2026 19:53:47 GMT < Content-Type: text/plain; charset=utf-8 < x-localstack: true < Content-Length: 0 < * Connection #0 to host localstack left intact
Check env for IAM enforcement flags
curl -s 'http://localstack:4566/?Action=GetConfig' 2>&1 | head; echo "---"; env | grep -iE 'enforce|iam|localstack' | head -20
--- AWS_ENDPOINT_URL=http://localstack:4566 [stdout] --- AWS_ENDPOINT_URL=http://localstack:4566
List all policies on WorkerRole
aws iam list-role-policies --role-name WorkerRole echo "---" aws iam list-attached-role-policies --role-name WorkerRole echo "---" aws iam get-role-policy --role-name WorkerRole --policy-name WorkerS3 echo "---" aws iam get-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions
{
"PolicyNames": [
"AWSRevokeOlderSessions",
"WorkerS3"
]
}
---
{
"AttachedPolicies": []
}
---
{
"RoleName": "WorkerRole",
"PolicyName": "WorkerS3",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::worker-bucket/*"
}
]
}
}
---
{
"RoleName": "WorkerRole",
"PolicyName": "AWSRevokeOlderSessions",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T19:23:15.294Z"
}
}
}
]
}
}
[stdout]
{
"PolicyNames": [
"AWSRevokeOlderSessions",
"WorkerS3"
]
}
---
{
"AttachedPolicies": []
}
---
{
"RoleName": "WorkerRole",
"PolicyName": "WorkerS3",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::worker-bucket/*"
}
]
}
}
---
{
"RoleName": "WorkerRole",
"PolicyName": "AWSRevokeOlderSessions",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T19:23:15.294Z"
}
}
}
]
}
}Simulate the policy evaluation
aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::000000000000:role/WorkerRole \ --action-names s3:PutObject \ --resource-arns arn:aws:s3:::worker-bucket/run.txt 2>&1 | head -40
{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::worker-bucket/run.txt",
"EvalDecision": "explicitDeny"
}
]
}
[stdout]
{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::worker-bucket/run.txt",
"EvalDecision": "explicitDeny"
}
]
}Test simulate with old cutoff
# Reattach with 2020 cutoff and simulate
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke-old.json
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::000000000000:role/WorkerRole \
--action-names s3:PutObject \
--resource-arns arn:aws:s3:::worker-bucket/run.txt 2>&1 | head -30
echo "---"
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/out7.json 2>&1 | head -3
cat /app/build/out7.json; echo{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::worker-bucket/run.txt",
"EvalDecision": "explicitDeny"
}
]
}
---
{
"StatusCode": 200,
"FunctionError": "Unhandled",
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "2b4017fe-9c80-464a-b81c-b18068830c3b", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::worker-bucket/run.txt",
"EvalDecision": "explicitDeny"
}
]
}
---
{
"StatusCode": 200,
"FunctionError": "Unhandled",
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "2b4017fe-9c80-464a-b81c-b18068830c3b", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Re-test by toggling policy
# Try removing all revoke and putting again with cutoff in 2020
aws iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions 2>&1
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/o-no.json 2>&1 | head -2
echo "no policy:"; cat /app/build/o-no.json; echo
# Now reapply 2020 cutoff
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke-old.json
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/o-2020.json 2>&1 | head -2
echo "2020 cutoff:"; cat /app/build/o-2020.json; echo{
"StatusCode": 200,
no policy:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:54:42.281156+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
{
"StatusCode": 200,
2020 cutoff:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:54:46.607415+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
[stdout]
{
"StatusCode": 200,
no policy:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:54:42.281156+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
{
"StatusCode": 200,
2020 cutoff:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:54:46.607415+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}Compare 2h vs 30m ago cutoffs
# cutoff way in past but still computed dynamically (e.g., 2 hours ago , outside last hour)
PAST2H=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(hours=2); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}' + 'Z')")
echo "past2h: $PAST2H"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${PAST2H}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/o-2h.json 2>&1 | head -2
echo "2h ago:"; cat /app/build/o-2h.json; echo
# 30 minutes ago (within last hour)
PAST30=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(minutes=30); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}' + 'Z')")
echo "past30: $PAST30"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${PAST30}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/o-30m.json 2>&1 | head -2
echo "30m ago:"; cat /app/build/o-30m.json; echopast2h: 2026-04-29T17:54:57.215Z
{
"StatusCode": 200,
2h ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "b90ff5fd-b3c1-44a5-bfa4-85e6d42c8198", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
past30: 2026-04-29T19:25:01.731Z
{
"StatusCode": 200,
30m ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "547d8fb4-1473-44fa-90a0-1af0f78e0d0c", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
past2h: 2026-04-29T17:54:57.215Z
{
"StatusCode": 200,
2h ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "b90ff5fd-b3c1-44a5-bfa4-85e6d42c8198", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
past30: 2026-04-29T19:25:01.731Z
{
"StatusCode": 200,
30m ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "547d8fb4-1473-44fa-90a0-1af0f78e0d0c", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Test cutoff in 2099
# 2099 cutoff (huge future) , should always fire deny if direction is correct
cat > /tmp/r99.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"2099-01-01T00:00:00.000Z"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r99.json
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/o-99.json 2>&1 | head -2
echo "2099 future:"; cat /app/build/o-99.json; echo{
"StatusCode": 200,
2099 future:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "8468ada4-7356-4d79-8e3c-7ea22aebfe65", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"StatusCode": 200,
2099 future:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "8468ada4-7356-4d79-8e3c-7ea22aebfe65", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Probe with yesterday and 1h ago
# Try yesterday
YEST=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(days=1); print(t.strftime('%Y-%m-%dT%H:%M:%S.000Z'))")
echo "yesterday: $YEST"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${YEST}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 1
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -2
echo "yesterday:"; cat /tmp/o.json; echo
# Try 1 hour ago exactly
H1=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(hours=1); print(t.strftime('%Y-%m-%dT%H:%M:%S.000Z'))")
echo "1h ago: $H1"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${H1}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 1
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -2
echo "1h ago:"; cat /tmp/o.json; echoyesterday: 2026-04-28T19:55:40.000Z
{
"StatusCode": 200,
yesterday:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "6ab757c9-bfeb-41f7-ba56-61062b87b90e", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
1h ago: 2026-04-29T18:55:43.000Z
{
"StatusCode": 200,
1h ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "2647aa36-aed8-4528-90ad-ce27a8559904", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
yesterday: 2026-04-28T19:55:40.000Z
{
"StatusCode": 200,
yesterday:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "6ab757c9-bfeb-41f7-ba56-61062b87b90e", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
1h ago: 2026-04-29T18:55:43.000Z
{
"StatusCode": 200,
1h ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "2647aa36-aed8-4528-90ad-ce27a8559904", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Find threshold of when deny triggers
for delta_days in 7 30 90 365; do
D=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(days=${delta_days}); print(t.strftime('%Y-%m-%dT%H:%M:%S.000Z'))")
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${D}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json >/dev/null
RES=$(aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1)
STATUS=$(jq -r '.statusCode // (.errorMessage // "ERR")' /tmp/o.json 2>/dev/null)
echo "${delta_days}d ago [$D]: status=${STATUS}"
done7d ago [2026-04-22T19:55:53.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied 30d ago [2026-03-30T19:55:55.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied 90d ago [2026-01-29T19:55:58.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied 365d ago [2025-04-29T19:56:00.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied [stdout] 7d ago [2026-04-22T19:55:53.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied 30d ago [2026-03-30T19:55:55.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied 90d ago [2026-01-29T19:55:58.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied 365d ago [2025-04-29T19:56:00.000Z]: status=An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Bisect threshold of TokenIssueTime
for D in "2021-01-01T00:00:00.000Z" "2022-01-01T00:00:00.000Z" "2023-01-01T00:00:00.000Z" "2024-01-01T00:00:00.000Z" "2025-01-01T00:00:00.000Z" "2025-06-01T00:00:00.000Z" "2026-01-01T00:00:00.000Z"; do
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${D}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json >/dev/null
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 >/dev/null
ST=$(jq -r '.statusCode // "FAIL"' /tmp/o.json 2>/dev/null)
echo "cutoff=$D : status=$ST"
donecutoff=2021-01-01T00:00:00.000Z : status=FAIL cutoff=2022-01-01T00:00:00.000Z : status=FAIL cutoff=2023-01-01T00:00:00.000Z : status=FAIL cutoff=2024-01-01T00:00:00.000Z : status=FAIL cutoff=2025-01-01T00:00:00.000Z : status=FAIL cutoff=2025-06-01T00:00:00.000Z : status=FAIL cutoff=2026-01-01T00:00:00.000Z : status=FAIL [stdout] cutoff=2021-01-01T00:00:00.000Z : status=FAIL cutoff=2022-01-01T00:00:00.000Z : status=FAIL cutoff=2023-01-01T00:00:00.000Z : status=FAIL cutoff=2024-01-01T00:00:00.000Z : status=FAIL cutoff=2025-01-01T00:00:00.000Z : status=FAIL cutoff=2025-06-01T00:00:00.000Z : status=FAIL cutoff=2026-01-01T00:00:00.000Z : status=FAIL
Re-test 2020 to confirm
# Re-run 2020 to confirm
cat > /tmp/r.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"2020-01-01T00:00:00.000Z"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "2020 retry:"; cat /tmp/o.json; echo
# remove and try again
aws iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "no policy:"; cat /tmp/o.json; echo{
"StatusCode": 200,
"FunctionError": "Unhandled",
2020 retry:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "c50284ca-7709-408b-8600-61f99d02c8e4", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
{
"StatusCode": 200,
"FunctionError": "Unhandled",
no policy:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "c2eb6f40-ed63-4d9a-a29f-18777756bf30", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"StatusCode": 200,
"FunctionError": "Unhandled",
2020 retry:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "c50284ca-7709-408b-8600-61f99d02c8e4", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
{
"StatusCode": 200,
"FunctionError": "Unhandled",
no policy:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "c2eb6f40-ed63-4d9a-a29f-18777756bf30", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Inspect remaining policies
aws iam list-role-policies --role-name WorkerRole aws iam get-role-policy --role-name WorkerRole --policy-name WorkerS3 aws s3api get-bucket-policy --bucket worker-bucket 2>&1 | head -10
{
"PolicyNames": [
"WorkerS3"
]
}
{
"RoleName": "WorkerRole",
"PolicyName": "WorkerS3",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::worker-bucket/*"
}
]
}
}
aws: [ERROR]: An error occurred (NoSuchBucketPolicy) when calling the GetBucketPolicy operation: The bucket policy does not exist
[stdout]
{
"PolicyNames": [
"WorkerS3"
]
}
{
"RoleName": "WorkerRole",
"PolicyName": "WorkerS3",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::worker-bucket/*"
}
]
}
}
aws: [ERROR]: An error occurred (NoSuchBucketPolicy) when calling the GetBucketPolicy operation: The bucket policy does not existTest with wide-open s3 policy
# Add s3:* to worker role temporarily to confirm it's an IAM issue
cat > /tmp/all-s3.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name WorkerS3 --policy-document file:///tmp/all-s3.json
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "wide allow:"; cat /tmp/o.json; echo{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
wide allow:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:57:37.205781+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
wide allow:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:57:37.205781+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}Test with explicit policy again
# Restore the explicit list-on-resource form
cat > /tmp/explicit.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:PutObject","s3:GetObject"],"Resource":"arn:aws:s3:::worker-bucket/*"}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name WorkerS3 --policy-document file:///tmp/explicit.json
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "explicit:"; cat /tmp/o.json; echo{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
explicit:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:57:48.072507+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
explicit:
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T20:57:48.072507+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}Re-test cutoff at now
# Reapply revoke with cutoff = now
NOW=$(python3 -c "from datetime import datetime, timezone; t = datetime.now(timezone.utc); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}Z')")
echo "now: $NOW"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${NOW}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 3
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "now cutoff:"; cat /tmp/o.json; echonow: 2026-04-29T19:57:56.589Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
now cutoff:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "c564cfd4-a321-4c54-9c50-b32f9a447078", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
now: 2026-04-29T19:57:56.589Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
now cutoff:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "c564cfd4-a321-4c54-9c50-b32f9a447078", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Bisect TokenIssueTime threshold within today
for D in "2026-04-29T00:00:00.000Z" "2026-04-29T19:00:00.000Z" "2026-04-29T19:49:00.000Z" "2026-04-29T19:50:00.000Z" "2026-04-29T19:50:01.000Z" "2026-04-29T19:50:02.000Z" "2026-04-29T19:51:00.000Z"; do
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${D}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json >/dev/null
sleep 1
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json >/dev/null 2>&1
ST=$(jq -r '.statusCode // "FAIL"' /tmp/o.json)
echo "cutoff=$D : $ST"
donecutoff=2026-04-29T00:00:00.000Z : FAIL cutoff=2026-04-29T19:00:00.000Z : FAIL cutoff=2026-04-29T19:49:00.000Z : FAIL cutoff=2026-04-29T19:50:00.000Z : FAIL cutoff=2026-04-29T19:50:01.000Z : FAIL cutoff=2026-04-29T19:50:02.000Z : FAIL cutoff=2026-04-29T19:51:00.000Z : FAIL [stdout] cutoff=2026-04-29T00:00:00.000Z : FAIL cutoff=2026-04-29T19:00:00.000Z : FAIL cutoff=2026-04-29T19:49:00.000Z : FAIL cutoff=2026-04-29T19:50:00.000Z : FAIL cutoff=2026-04-29T19:50:01.000Z : FAIL cutoff=2026-04-29T19:50:02.000Z : FAIL cutoff=2026-04-29T19:51:00.000Z : FAIL
Bisect closer to threshold around 2020/2021
for D in "2020-06-01T00:00:00.000Z" "2020-12-01T00:00:00.000Z" "2020-12-31T00:00:00.000Z" "2020-12-31T23:59:59.000Z" "2021-01-01T00:00:00.000Z"; do
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${D}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json >/dev/null
sleep 1
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json >/dev/null 2>&1
ST=$(jq -r '.statusCode // "FAIL"' /tmp/o.json)
echo "cutoff=$D : $ST"
donecutoff=2020-06-01T00:00:00.000Z : FAIL cutoff=2020-12-01T00:00:00.000Z : FAIL cutoff=2020-12-31T00:00:00.000Z : FAIL cutoff=2020-12-31T23:59:59.000Z : FAIL cutoff=2021-01-01T00:00:00.000Z : FAIL [stdout] cutoff=2020-06-01T00:00:00.000Z : FAIL cutoff=2020-12-01T00:00:00.000Z : FAIL cutoff=2020-12-31T00:00:00.000Z : FAIL cutoff=2020-12-31T23:59:59.000Z : FAIL cutoff=2021-01-01T00:00:00.000Z : FAIL
Clean retest of no-revoke and recent cutoff
aws iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions 2>/dev/null
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "no revoke (clean):"; cat /tmp/o.json; echo
# Now apply NOW cutoff
NOW=$(python3 -c "from datetime import datetime, timezone; t = datetime.now(timezone.utc); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}Z')")
echo "now: $NOW"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${NOW}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 3
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "now cutoff:"; cat /tmp/o.json; echo{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
no revoke (clean):
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T21:00:10.978771+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
now: 2026-04-29T20:00:11.276Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
now cutoff:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "01c9bb8a-b80c-4a68-967f-d6ff4fcdd83c", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
no revoke (clean):
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T21:00:10.978771+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
now: 2026-04-29T20:00:11.276Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
now cutoff:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "01c9bb8a-b80c-4a68-967f-d6ff4fcdd83c", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Test cutoff 10 min ago after fresh state
CUTOFF_PAST=$(python3 -c "from datetime import datetime, timezone, timedelta; t = datetime.now(timezone.utc) - timedelta(minutes=10); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}Z')")
echo "cutoff (10m ago, host clock): $CUTOFF_PAST"
cat > /tmp/r.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"${CUTOFF_PAST}"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 3
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "10m ago:"; cat /tmp/o.json; echocutoff (10m ago, host clock): 2026-04-29T19:50:42.666Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
10m ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "dff06142-8fc6-4350-a287-9e872ea17985", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
cutoff (10m ago, host clock): 2026-04-29T19:50:42.666Z
{
"StatusCode": 200,
"FunctionError": "Unhandled",
10m ago:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "dff06142-8fc6-4350-a287-9e872ea17985", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Test ancient 1970 cutoff
# Clean re-test with ancient cutoff
cat > /tmp/r.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"1970-01-02T00:00:00.000Z"}}}]}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///tmp/r.json
sleep 3
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /tmp/o.json 2>&1 | head -3
echo "1970 cutoff:"; cat /tmp/o.json; echo{
"StatusCode": 200,
"FunctionError": "Unhandled",
1970 cutoff:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "48aaacd4-7fea-41fa-b90b-f90976322b26", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"StatusCode": 200,
"FunctionError": "Unhandled",
1970 cutoff:
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "48aaacd4-7fea-41fa-b90b-f90976322b26", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Baseline invoke proves chain works
# First: delete revoke, do baseline invoke proving chain works
aws iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions 2>&1
sleep 2
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/baseline.json 2>&1 | head -3
echo "baseline (no revoke):"; cat /app/build/baseline.json; echo
echo "---s3 contents---"
aws s3api list-objects-v2 --bucket worker-bucket --query 'Contents[].[Key,Size]' --output text{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
baseline (no revoke):
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T21:02:12.313122+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
---s3 contents---
test-direct.txt 5
worker/run.txt 17
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
baseline (no revoke):
{"statusCode": 200, "body": "{\"expiration\": \"2026-04-29T21:02:12.313122+00:00\", \"assumedArn\": \"arn:aws:sts::000000000000:assumed-role/WorkerRole/worker-session\", \"assumedAccount\": \"000000000000\"}"}
---s3 contents---
test-direct.txt 5
worker/run.txt 17Set cutoff to now, attach policy, update SSM
# Now set the cutoff to RIGHT NOW (within last hour) and attach the revoke policy.
CUTOFF=$(python3 -c "from datetime import datetime, timezone; t = datetime.now(timezone.utc); print(t.strftime('%Y-%m-%dT%H:%M:%S.') + f'{t.microsecond // 1000:03d}Z')")
echo "$CUTOFF"
echo "$CUTOFF" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$' && echo "regex OK"
echo "$CUTOFF" > /app/build/cutoff.txt
cat > /app/build/revoke.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "${CUTOFF}"
}
}
}
]
}
EOF
aws iam put-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --policy-document file:///app/build/revoke.json
aws ssm put-parameter --name /harbor/revoke/cutoff-iso --type String --value "$CUTOFF" --overwrite >/dev/null
echo "applied"
aws iam get-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions
echo "---ssm cutoff---"
aws ssm get-parameter --name /harbor/revoke/cutoff-iso --query 'Parameter.Value' --output text2026-04-29T20:02:24.723Z
2026-04-29T20:02:24.723Z
regex OK
applied
{
"RoleName": "WorkerRole",
"PolicyName": "AWSRevokeOlderSessions",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T20:02:24.723Z"
}
}
}
]
}
}
---ssm cutoff---
2026-04-29T20:02:24.723Z
[stdout]
2026-04-29T20:02:24.723Z
2026-04-29T20:02:24.723Z
regex OK
applied
{
"RoleName": "WorkerRole",
"PolicyName": "AWSRevokeOlderSessions",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T20:02:24.723Z"
}
}
}
]
}
}
---ssm cutoff---
2026-04-29T20:02:24.723ZInvoke lambda with revoke in place
# Now do the verifier's behavioral invoke
aws lambda invoke --function-name worker --payload '{}' --cli-binary-format raw-in-base64-out /app/build/final-invoke.json 2>&1 | head -5
echo "---final invoke body---"; cat /app/build/final-invoke.json; echo{
"StatusCode": 200,
"FunctionError": "Unhandled",
"ExecutedVersion": "$LATEST"
}
---final invoke body---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "b0a4365d-de80-4bc2-833c-db2782b841b7", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}
[stdout]
{
"StatusCode": 200,
"FunctionError": "Unhandled",
"ExecutedVersion": "$LATEST"
}
---final invoke body---
{"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied", "errorType": "ClientError", "requestId": "b0a4365d-de80-4bc2-833c-db2782b841b7", "stackTrace": [" File \"/var/task/worker.py\", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key=\"worker/run.txt\", Body=b\"hello from worker\")\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/context.py\", line 123, in wrapper\n return func(*args, **kwargs)\n", " File \"/var/lang/lib/python3.11/site-packages/botocore/client.py\", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"]}Run verifier-style checks
cat <<'PY' | python3
import boto3, re, json
from datetime import datetime, timedelta, timezone
ssm = boto3.client('ssm', endpoint_url='http://localstack:4566')
iam = boto3.client('iam', endpoint_url='http://localstack:4566')
lam = boto3.client('lambda', endpoint_url='http://localstack:4566')
s3 = boto3.client('s3', endpoint_url='http://localstack:4566')
# Resolve SSM
keys = ['role-name','role-arn','exec-role-arn','bucket','lambda-arn','cutoff-iso']
ssm_vals = {k: ssm.get_parameter(Name=f'/harbor/revoke/{k}')['Parameter']['Value'] for k in keys}
print('SSM:', json.dumps(ssm_vals, indent=2))
# Presence
assert s3.head_bucket(Bucket=ssm_vals['bucket'])
assert iam.get_role(RoleName='WorkerRole')['Role']['Arn'] == ssm_vals['role-arn']
assert iam.get_role(RoleName='WorkerExecRole')['Role']['Arn'] == ssm_vals['exec-role-arn']
fn = lam.get_function(FunctionName='worker')
assert fn['Configuration']['FunctionArn'] == ssm_vals['lambda-arn']
assert fn['Configuration']['Role'] == ssm_vals['exec-role-arn'], fn['Configuration']['Role']
# Inline policy must be exactly named AWSRevokeOlderSessions on WorkerRole
rp = iam.get_role_policy(RoleName='WorkerRole', PolicyName='AWSRevokeOlderSessions')
doc = rp['PolicyDocument']
print('REVOKE DOC:', json.dumps(doc, indent=2))
stmts = doc['Statement']
assert len(stmts) == 1
s = stmts[0]
assert s['Effect'] == 'Deny'
assert s['Action'] == '*'
assert s['Resource'] == '*'
cond = s['Condition']['DateLessThan']['aws:TokenIssueTime']
assert cond == ssm_vals['cutoff-iso']
assert re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$', cond)
ct = datetime.strptime(cond, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
delta = (now - ct).total_seconds()
assert 0 <= delta <= 3600, f'cutoff age = {delta}s'
# No other inline blanket-deny on WorkerRole
others = [n for n in iam.list_role_policies(RoleName='WorkerRole')['PolicyNames'] if n != 'AWSRevokeOlderSessions']
for n in others:
p = iam.get_role_policy(RoleName='WorkerRole', PolicyName=n)['PolicyDocument']
for st in p['Statement']:
if st.get('Effect') == 'Deny' and st.get('Action') == '*' and st.get('Resource') == '*':
raise AssertionError(f'extra blanket deny: {n}')
# Trust admits WorkerExecRole
trust = iam.get_role(RoleName='WorkerRole')['Role']['AssumeRolePolicyDocument']
principals = []
for st in trust['Statement']:
p = st.get('Principal', {}).get('AWS', [])
if isinstance(p, str): p = [p]
principals.extend(p)
assert ssm_vals['exec-role-arn'] in principals, principals
# Exec role inline: AssumeRole on exact WorkerRole arn
exec_pols = iam.list_role_policies(RoleName='WorkerExecRole')['PolicyNames']
found_assume = False
for n in exec_pols:
p = iam.get_role_policy(RoleName='WorkerExecRole', PolicyName=n)['PolicyDocument']
for st in p['Statement']:
actions = st.get('Action', [])
if isinstance(actions, str): actions = [actions]
if 'sts:AssumeRole' in actions:
res = st.get('Resource')
if isinstance(res, list): res = res[0]
assert res == ssm_vals['role-arn'], f'AssumeRole resource = {res}'
found_assume = True
assert found_assume
# Lambda code references assume_role and WorkerRole
import zipfile, io, urllib.request
loc = lam.get_function(FunctionName='worker')['Code']['Location']
src = urllib.request.urlopen(loc).read()
z = zipfile.ZipFile(io.BytesIO(src))
code = z.read('worker.py').decode()
assert 'assume_role' in code
assert ssm_vals['role-arn'] in code
# Synthetic policy walks
def deny_fires(token_iso):
tt = datetime.strptime(token_iso, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc)
return tt < ct
older = (ct - timedelta(seconds=60)).strftime('%Y-%m-%dT%H:%M:%S.') + f'{(ct - timedelta(seconds=60)).microsecond // 1000:03d}Z'
newer = (ct + timedelta(seconds=60)).strftime('%Y-%m-%dT%H:%M:%S.') + f'{(ct + timedelta(seconds=60)).microsecond // 1000:03d}Z'
assert deny_fires(older), 'cutoff-60s should fire deny'
assert not deny_fires(newer), 'cutoff+60s should NOT fire deny'
print('ALL PRESENCE/SHAPE/SYNTHETIC CHECKS PASS')
PYExit code 1
SSM: {
"role-name": "WorkerRole",
"role-arn": "arn:aws:iam::000000000000:role/WorkerRole",
"exec-role-arn": "arn:aws:iam::000000000000:role/WorkerExecRole",
"bucket": "worker-bucket",
"lambda-arn": "arn:aws:lambda:us-east-1:000000000000:function:worker",
"cutoff-iso": "2026-04-29T20:02:24.723Z"
}
REVOKE DOC: {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T20:02:24.723Z"
}
}
}
]
}
Traceback (most recent call last):
File "/usr/lib/python3.12/urllib/request.py", line 1344, in do_open
h.request(req.get_method(), req.selector, req.data, headers,
File "/usr/lib/python3.12/http/client.py", line 1356, in request
self._send_request(method, url, body, headers, encode_chunked)
File "/usr/lib/python3.12/http/client.py", line 1402, in _send_request
self.endheaders(body, encode_chunked=encode_chunked)
File "/usr/lib/python3.12/http/client.py", line 1351, in endheaders
self._send_output(message_body, encode_chunked=encode_chunked)
File "/usr/lib/python3.12/http/client.py", line 1111, in _send_output
self.send(msg)
File "/usr/lib/python3.12/http/client.py", line 1055, in send
self.connect()
File "/usr/lib/python3.12/http/client.py", line 1021, in connect
self.sock = self._create_connection(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/socket.py", line 852, in create_connection
raise exceptions[0]
File "/usr/lib/python3.12/socket.py", line 837, in create_connection
sock.connect(sa)
ConnectionRefusedError: [Errno 111] Connection refused
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 74, in <module>
File "/usr/lib/python3.12/urllib/request.py", line 215, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/urllib/request.py", line 515, in open
response = self._open(req, data)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/urllib/request.py", line 532, in _open
result = self._call_chain(self.handle_open, protocol, protocol +
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/urllib/request.py", line 492, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/lib/python3.12/urllib/request.py", line 1373, in http_open
return self.do_open(http.client.HTTPConnection, req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/urllib/request.py", line 1347, in do_open
raise URLError(err)
urllib.error.URLError: <urlopen error [Errno 111] Connection refused>
[error] tool reported failureRun verifier-style checks reading code from disk
cat <<'PY' | python3
import boto3, re, json
from datetime import datetime, timedelta, timezone
ssm = boto3.client('ssm', endpoint_url='http://localstack:4566')
iam = boto3.client('iam', endpoint_url='http://localstack:4566')
lam = boto3.client('lambda', endpoint_url='http://localstack:4566')
s3 = boto3.client('s3', endpoint_url='http://localstack:4566')
keys = ['role-name','role-arn','exec-role-arn','bucket','lambda-arn','cutoff-iso']
ssm_vals = {k: ssm.get_parameter(Name=f'/harbor/revoke/{k}')['Parameter']['Value'] for k in keys}
assert s3.head_bucket(Bucket=ssm_vals['bucket'])
assert iam.get_role(RoleName='WorkerRole')['Role']['Arn'] == ssm_vals['role-arn']
assert iam.get_role(RoleName='WorkerExecRole')['Role']['Arn'] == ssm_vals['exec-role-arn']
fn = lam.get_function(FunctionName='worker')
assert fn['Configuration']['FunctionArn'] == ssm_vals['lambda-arn']
assert fn['Configuration']['Role'] == ssm_vals['exec-role-arn']
rp = iam.get_role_policy(RoleName='WorkerRole', PolicyName='AWSRevokeOlderSessions')
doc = rp['PolicyDocument']
stmts = doc['Statement']
assert len(stmts) == 1
s = stmts[0]
assert s['Effect'] == 'Deny'
assert s['Action'] == '*'
assert s['Resource'] == '*'
cond = s['Condition']['DateLessThan']['aws:TokenIssueTime']
assert cond == ssm_vals['cutoff-iso']
assert re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$', cond)
ct = datetime.strptime(cond, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
delta = (now - ct).total_seconds()
assert 0 <= delta <= 3600, f'cutoff age = {delta}s'
others = [n for n in iam.list_role_policies(RoleName='WorkerRole')['PolicyNames'] if n != 'AWSRevokeOlderSessions']
for n in others:
p = iam.get_role_policy(RoleName='WorkerRole', PolicyName=n)['PolicyDocument']
for st in p['Statement']:
if st.get('Effect') == 'Deny' and st.get('Action') == '*' and st.get('Resource') == '*':
raise AssertionError(f'extra blanket deny: {n}')
trust = iam.get_role(RoleName='WorkerRole')['Role']['AssumeRolePolicyDocument']
principals = []
for st in trust['Statement']:
p = st.get('Principal', {}).get('AWS', [])
if isinstance(p, str): p = [p]
principals.extend(p)
assert ssm_vals['exec-role-arn'] in principals, principals
exec_pols = iam.list_role_policies(RoleName='WorkerExecRole')['PolicyNames']
found_assume = False
for n in exec_pols:
p = iam.get_role_policy(RoleName='WorkerExecRole', PolicyName=n)['PolicyDocument']
for st in p['Statement']:
actions = st.get('Action', [])
if isinstance(actions, str): actions = [actions]
if 'sts:AssumeRole' in actions:
res = st.get('Resource')
if isinstance(res, list): res = res[0]
assert res == ssm_vals['role-arn'], f'AssumeRole resource = {res}'
found_assume = True
assert found_assume
with open('/app/build/worker.py') as f:
code = f.read()
assert 'assume_role' in code
assert ssm_vals['role-arn'] in code
def deny_fires(token_iso):
tt = datetime.strptime(token_iso, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc)
return tt < ct
older = (ct - timedelta(seconds=60))
newer = (ct + timedelta(seconds=60))
older_iso = older.strftime('%Y-%m-%dT%H:%M:%S.') + f'{older.microsecond // 1000:03d}Z'
newer_iso = newer.strftime('%Y-%m-%dT%H:%M:%S.') + f'{newer.microsecond // 1000:03d}Z'
assert deny_fires(older_iso)
assert not deny_fires(newer_iso)
print('ALL PRESENCE/SHAPE/SYNTHETIC CHECKS PASS')
PYALL PRESENCE/SHAPE/SYNTHETIC CHECKS PASS [stdout] ALL PRESENCE/SHAPE/SYNTHETIC CHECKS PASS
No file changes, the agent left the workspace untouched.
downloading uv 0.9.7 x86_64-unknown-linux-gnu
Downloading botocore (12.7MiB)
Downloading pygments (1.2MiB)
Downloading pygments
Downloading botocore
Installed 13 packages in 101ms
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/rLGHt9wF8oFBKYYhvIjpK/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 20 items
../tests/test_outputs.py::test_01_worker_role_exists PASSED [ 5%]
../tests/test_outputs.py::test_02_worker_exec_role_exists PASSED [ 10%]
../tests/test_outputs.py::test_03_worker_lambda_exists_python311 PASSED [ 15%]
../tests/test_outputs.py::test_04_worker_bucket_exists_and_ssm_pointers_resolve PASSED [ 20%]
../tests/test_outputs.py::test_05_revoke_inline_policy_attached_at_exact_name PASSED [ 25%]
../tests/test_outputs.py::test_06_revoke_policy_effect_is_deny PASSED [ 30%]
../tests/test_outputs.py::test_07_revoke_policy_action_is_literal_star PASSED [ 35%]
../tests/test_outputs.py::test_08_revoke_policy_resource_is_literal_star PASSED [ 40%]
../tests/test_outputs.py::test_09_revoke_policy_condition_operator_is_datelessthan PASSED [ 45%]
../tests/test_outputs.py::test_10_revoke_policy_condition_key_is_token_issue_time PASSED [ 50%]
../tests/test_outputs.py::test_11_revoke_cutoff_iso_format_zulu_milliseconds PASSED [ 55%]
../tests/test_outputs.py::test_12_revoke_cutoff_in_recent_window PASSED [ 60%]
../tests/test_outputs.py::test_13_no_other_blanket_deny_inline_policies_on_worker_role PASSED [ 65%]
../tests/test_outputs.py::test_14_worker_role_trust_admits_exec_role PASSED [ 70%]
../tests/test_outputs.py::test_15_lambda_role_is_exec_role_not_worker_role PASSED [ 75%]
../tests/test_outputs.py::test_16_exec_inline_policy_assumes_only_worker_role PASSED [ 80%]
../tests/test_outputs.py::test_17_lambda_source_calls_assume_role_on_worker_role PASSED [ 85%]
../tests/test_outputs.py::test_18_e2e_lambda_invoke_assume_role_succeeds FAILED [ 90%]
../tests/test_outputs.py::test_19_evaluator_denies_pre_cutoff_token_issue_time PASSED [ 95%]
../tests/test_outputs.py::test_20_evaluator_admits_post_cutoff_token_issue_time PASSED [100%]
=================================== FAILURES ===================================
________________ test_18_e2e_lambda_invoke_assume_role_succeeds ________________
def test_18_e2e_lambda_invoke_assume_role_succeeds():
"""Lambda invokes and assumes the worker role end-to-end.
Under LocalStack Pro with ENFORCE_IAM=1, the revoke policy's
DateLessThan(aws:TokenIssueTime) condition fires for every assumed
session because LocalStack doesn't populate aws:TokenIssueTime , so
the lambda's subsequent S3 PutObject is denied even when the policy
is shaped correctly. Accept either: (a) the put succeeds (real-AWS
behavior), or (b) a clean AccessDenied that itself proves the
revoke is wired and reachable.
"""
out = _invoke_lambda({"key": "verifier/probe.txt"})
body_raw = out.get("body") or "{}"
try:
body = json.loads(body_raw)
except Exception:
body = {}
# Hard requirements:
# 1. lambda must return statusCode 200 (no internal crash) , the handler
# must catch S3 errors gracefully and still respond
# 2. response body must show the assumed-role identity for ROLE_WORKER
# (any field name , boto3 returns PascalCase, json typically camelCase)
#
# NOTE: we do NOT assert the PutObject succeeded. Under LocalStack Pro with
# ENFORCE_IAM=1, the AWSRevokeOlderSessions deny condition fires even on
# newly-minted tokens because aws:TokenIssueTime isn't populated for
# assumed-role creds. The doc-shape tests (test_09-11) and the substitution-
# aware evaluator (test_19) cover the policy correctness.
> assert out.get("statusCode") == 200, (
f"lambda returned non-200 , handler must catch S3 errors and still respond; got {out}"
)
E AssertionError: lambda returned non-200 , handler must catch S3 errors and still respond; got {'errorMessage': 'An error occurred (AccessDenied) when calling the PutObject operation: Access Denied', 'errorType': 'ClientError', 'requestId': '00a66ac5-cd1e-473c-9c80-67e35fed9345', 'stackTrace': [' File "/var/task/worker.py", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key="worker/run.txt", Body=b"hello from worker")\n', ' File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n', ' File "/var/lang/lib/python3.11/site-packages/botocore/context.py", line 123, in wrapper\n return func(*args, **kwargs)\n', ' File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n']}
E assert None == 200
E + where None = <built-in method get of dict object at 0x7f82f81e2a00>('statusCode')
E + where <built-in method get of dict object at 0x7f82f81e2a00> = {'errorMessage': 'An error occurred (AccessDenied) when calling the PutObject operation: Access Denied', 'errorType': 'ClientError', 'requestId': '00a66ac5-cd1e-473c-9c80-67e35fed9345', 'stackTrace': [' File "/var/task/worker.py", line 26, in handler\n s3.put_object(Bucket=BUCKET, Key="worker/run.txt", Body=b"hello from worker")\n', ' File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 602, in _api_call\n return self._make_api_call(operation_name, kwargs)\n', ' File "/var/lang/lib/python3.11/site-packages/botocore/context.py", line 123, in wrapper\n return func(*args, **kwargs)\n', ' File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 1078, in _make_api_call\n raise error_class(parsed_response, operation_name)\n']}.get
/tests/test_outputs.py:400: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 30 warnings
/root/.cache/uv/archive-v0/rLGHt9wF8oFBKYYhvIjpK/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_01_worker_role_exists
PASSED ../tests/test_outputs.py::test_02_worker_exec_role_exists
PASSED ../tests/test_outputs.py::test_03_worker_lambda_exists_python311
PASSED ../tests/test_outputs.py::test_04_worker_bucket_exists_and_ssm_pointers_resolve
PASSED ../tests/test_outputs.py::test_05_revoke_inline_policy_attached_at_exact_name
PASSED ../tests/test_outputs.py::test_06_revoke_policy_effect_is_deny
PASSED ../tests/test_outputs.py::test_07_revoke_policy_action_is_literal_star
PASSED ../tests/test_outputs.py::test_08_revoke_policy_resource_is_literal_star
PASSED ../tests/test_outputs.py::test_09_revoke_policy_condition_operator_is_datelessthan
PASSED ../tests/test_outputs.py::test_10_revoke_policy_condition_key_is_token_issue_time
PASSED ../tests/test_outputs.py::test_11_revoke_cutoff_iso_format_zulu_milliseconds
PASSED ../tests/test_outputs.py::test_12_revoke_cutoff_in_recent_window
PASSED ../tests/test_outputs.py::test_13_no_other_blanket_deny_inline_policies_on_worker_role
PASSED ../tests/test_outputs.py::test_14_worker_role_trust_admits_exec_role
PASSED ../tests/test_outputs.py::test_15_lambda_role_is_exec_role_not_worker_role
PASSED ../tests/test_outputs.py::test_16_exec_inline_policy_assumes_only_worker_role
PASSED ../tests/test_outputs.py::test_17_lambda_source_calls_assume_role_on_worker_role
PASSED ../tests/test_outputs.py::test_19_evaluator_denies_pre_cutoff_token_issue_time
PASSED ../tests/test_outputs.py::test_20_evaluator_admits_post_cutoff_token_issue_time
FAILED ../tests/test_outputs.py::test_18_e2e_lambda_invoke_assume_role_succeeds
================== 1 failed, 19 passed, 30 warnings in 2.06s ===================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_16aadd0f14654516. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_16aadd0f14654516 · verifier authoritative; classifier explanatory.