tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Inspect working dir and tooling
ls -la && which aws python3 jq zip
total 8 drwxr-xr-x 2 root root 4096 Apr 29 20:24 . drwxr-xr-x 1 root root 4096 Apr 29 20:24 .. /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/jq /usr/bin/zip [stdout] total 8 drwxr-xr-x 2 root root 4096 Apr 29 20:24 . drwxr-xr-x 1 root root 4096 Apr 29 20:24 .. /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/jq /usr/bin/zip
Check LocalStack health for needed services
curl -s http://localstack:4566/_localstack/health | jq -r '.services | to_entries | map(select(.key=="iam" or .key=="lambda" or .key=="s3" or .key=="ssm" or .key=="sts")) | .[].key + ": " + .[].value'
iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available [stdout] iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available iam: available lambda: available s3: available ssm: available sts: available
Verify AWS credentials and STS
env | grep -i aws ; echo "---" ; aws --endpoint-url=http://localstack:4566 sts get-caller-identity
AWS_DEFAULT_REGION=us-east-1
AWS_PAGER=
AWS_SECRET_ACCESS_KEY=test
AWS_ACCESS_KEY_ID=test
AWS_ENDPOINT_URL=http://localstack:4566
---
{
"UserId": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
[stdout]
AWS_DEFAULT_REGION=us-east-1
AWS_PAGER=
AWS_SECRET_ACCESS_KEY=test
AWS_ACCESS_KEY_ID=test
AWS_ENDPOINT_URL=http://localstack:4566
---
{
"UserId": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}Create work directory
mkdir -p /app/work && cd /app/work && pwd
/app/work [stdout] /app/work
import json
import os
import boto3
WORKER_ROLE_ARN = os.environ["WORKER_ROLE_ARN"]
BUCKET = os.environ["BUCKET"]
ENDPOINT_URL = os.environ.get("AWS_ENDPOINT_URL")
def handler(event, context):
sts = boto3.client("sts", endpoint_url=ENDPOINT_URL)
assumed = sts.assume_role(
RoleArn=WORKER_ROLE_ARN,
RoleSessionName="worker-session",
DurationSeconds=3600,
)
creds = assumed["Credentials"]
s3 = boto3.client(
"s3",
endpoint_url=ENDPOINT_URL,
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
key = "worker/output.txt"
s3.put_object(Bucket=BUCKET, Key=key, Body=b"hello from worker")
sts_assumed = boto3.client(
"sts",
endpoint_url=ENDPOINT_URL,
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": creds["Expiration"].isoformat(),
"caller": {"Arn": ident["Arn"], "UserId": ident["UserId"]},
"bucket": BUCKET,
"key": key,
}
),
}
#!/usr/bin/env bash
set -euo pipefail
ENDPOINT="http://localstack:4566"
REGION="us-east-1"
ACCOUNT_ID="000000000000"
BUCKET="worker-bucket"
WORKER_ROLE="WorkerRole"
EXEC_ROLE="WorkerExecRole"
LAMBDA_NAME="worker"
WORKER_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${WORKER_ROLE}"
EXEC_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${EXEC_ROLE}"
aws() { command aws --endpoint-url="${ENDPOINT}" --region "${REGION}" "$@"; }
# ----- s3 bucket -----
aws s3api create-bucket --bucket "${BUCKET}" >/dev/null 2>&1 || true
# ----- WorkerExecRole (lambda's own runtime identity) -----
EXEC_TRUST=$(cat <<'JSON'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
JSON
)
aws iam create-role \
--role-name "${EXEC_ROLE}" \
--assume-role-policy-document "${EXEC_TRUST}" >/dev/null 2>&1 || \
aws iam update-assume-role-policy \
--role-name "${EXEC_ROLE}" \
--policy-document "${EXEC_TRUST}" >/dev/null
aws iam attach-role-policy \
--role-name "${EXEC_ROLE}" \
--policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" >/dev/null
EXEC_INLINE=$(cat <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "${WORKER_ROLE_ARN}"
}
]
}
JSON
)
aws iam put-role-policy \
--role-name "${EXEC_ROLE}" \
--policy-name "AssumeWorkerRole" \
--policy-document "${EXEC_INLINE}" >/dev/null
# ----- WorkerRole (assumed by the lambda's exec role) -----
WORKER_TRUST=$(cat <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "${EXEC_ROLE_ARN}" },
"Action": "sts:AssumeRole"
}
]
}
JSON
)
aws iam create-role \
--role-name "${WORKER_ROLE}" \
--assume-role-policy-document "${WORKER_TRUST}" >/dev/null 2>&1 || \
aws iam update-assume-role-policy \
--role-name "${WORKER_ROLE}" \
--policy-document "${WORKER_TRUST}" >/dev/null
WORKER_S3=$(cat <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::${BUCKET}/*"
}
]
}
JSON
)
aws iam put-role-policy \
--role-name "${WORKER_ROLE}" \
--policy-name "WorkerS3Access" \
--policy-document "${WORKER_S3}" >/dev/null
# ----- AWSRevokeOlderSessions inline policy on WorkerRole -----
# Cutoff: "right now" with millisecond ISO-8601, Z suffix.
CUTOFF_ISO="$(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: ${CUTOFF_ISO}"
REVOKE=$(cat <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "${CUTOFF_ISO}"
}
}
}
]
}
JSON
)
aws iam put-role-policy \
--role-name "${WORKER_ROLE}" \
--policy-name "AWSRevokeOlderSessions" \
--policy-document "${REVOKE}" >/dev/null
# ----- lambda zip + create -----
cd /app/work
rm -f lambda.zip
zip -q lambda.zip lambda_function.py
# wait briefly for role propagation
sleep 2
aws lambda delete-function --function-name "${LAMBDA_NAME}" >/dev/null 2>&1 || true
aws lambda create-function \
--function-name "${LAMBDA_NAME}" \
--runtime python3.11 \
--role "${EXEC_ROLE_ARN}" \
--handler "lambda_function.handler" \
--zip-file "fileb://lambda.zip" \
--timeout 30 \
--environment "Variables={WORKER_ROLE_ARN=${WORKER_ROLE_ARN},BUCKET=${BUCKET},AWS_ENDPOINT_URL=${ENDPOINT}}" \
>/dev/null
# wait for active state
for i in $(seq 1 30); do
state=$(aws lambda get-function --function-name "${LAMBDA_NAME}" --query 'Configuration.State' --output text 2>/dev/null || echo Pending)
[[ "${state}" == "Active" ]] && break
sleep 1
done
LAMBDA_ARN=$(aws lambda get-function --function-name "${LAMBDA_NAME}" --query 'Configuration.FunctionArn' --output text)
# ----- ssm pointers -----
aws ssm put-parameter --name "/harbor/revoke/role-name" --type String --overwrite --value "${WORKER_ROLE}" >/dev/null
aws ssm put-parameter --name "/harbor/revoke/role-arn" --type String --overwrite --value "${WORKER_ROLE_ARN}" >/dev/null
aws ssm put-parameter --name "/harbor/revoke/exec-role-arn" --type String --overwrite --value "${EXEC_ROLE_ARN}" >/dev/null
aws ssm put-parameter --name "/harbor/revoke/bucket" --type String --overwrite --value "${BUCKET}" >/dev/null
aws ssm put-parameter --name "/harbor/revoke/lambda-arn" --type String --overwrite --value "${LAMBDA_ARN}" >/dev/null
aws ssm put-parameter --name "/harbor/revoke/cutoff-iso" --type String --overwrite --value "${CUTOFF_ISO}" >/dev/null
echo "OK build complete"
echo "lambda arn: ${LAMBDA_ARN}"
/app/work/build.sh
CUTOFF_ISO="$(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")')"CUTOFF_ISO="$(python3 -c 'from datetime import datetime, timezone
n = datetime.now(timezone.utc)
print(n.strftime("%Y-%m-%dT%H:%M:%S.") + f"{n.microsecond // 1000:03d}Z")')"Run build script
chmod +x /app/work/build.sh && /app/work/build.sh
cutoff: 2026-04-29T20:26:09.091Z OK build complete lambda arn: arn:aws:lambda:us-east-1:000000000000:function:worker [stdout] cutoff: 2026-04-29T20:26:09.091Z OK build complete lambda arn: arn:aws:lambda:us-east-1:000000000000:function:worker
Run verification checks
cat <<'EOF' > verify.sh
#!/usr/bin/env bash
set -euo pipefail
ENDPOINT="http://localstack:4566"
aws() { command aws --endpoint-url="${ENDPOINT}" "$@"; }
echo "== presence =="
aws s3api head-bucket --bucket worker-bucket && echo "bucket OK"
aws iam get-role --role-name WorkerExecRole --query 'Role.Arn' --output text
aws iam get-role --role-name WorkerRole --query 'Role.Arn' --output text
aws lambda get-function --function-name worker --query 'Configuration.[Role,State]' --output text
echo "== ssm =="
for p in role-name role-arn exec-role-arn bucket lambda-arn cutoff-iso; do
v=$(aws ssm get-parameter --name "/harbor/revoke/${p}" --query 'Parameter.Value' --output text)
echo "${p}: ${v}"
done
echo "== revoke policy on WorkerRole =="
aws iam get-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions --query 'PolicyDocument' --output json
echo "== other inline policies on WorkerRole =="
aws iam list-role-policies --role-name WorkerRole --output json
echo "== exec role inline (assume-role only on WorkerRole arn) =="
aws iam get-role-policy --role-name WorkerExecRole --policy-name AssumeWorkerRole --query 'PolicyDocument' --output json
echo "== worker role trust =="
aws iam get-role --role-name WorkerRole --query 'Role.AssumeRolePolicyDocument' --output json
echo "== invoke =="
aws lambda invoke --function-name worker --payload '{}' /tmp/out.json >/dev/null
cat /tmp/out.json | jq .
EOF
chmod +x verify.sh && ./verify.sh== presence ==
{
"BucketArn": "arn:aws:s3:::worker-bucket",
"BucketRegion": "us-east-1"
}
bucket OK
arn:aws:iam::000000000000:role/WorkerExecRole
arn:aws:iam::000000000000:role/WorkerRole
arn:aws:iam::000000000000:role/WorkerExecRole Active
== 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:26:09.091Z
== revoke policy on WorkerRole ==
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T20:26:09.091Z"
}
}
}
]
}
== other inline policies on WorkerRole ==
{
"PolicyNames": [
"AWSRevokeOlderSessions",
"WorkerS3Access"
]
}
== exec role inline (assume-role only on WorkerRole arn) ==
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::000000000000:role/WorkerRole"
}
]
}
== worker role trust ==
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/WorkerExecRole"
},
"Action": "sts:AssumeRole"
}
]
}
== invoke ==
{
"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied",
"errorType": "ClientError",
"requestId": "c1807abf-21d1-4a26-ac58-1c6f297a2f1a",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 29, in handler\n s3.put_object(Bucket=BUCKET, Key=key, 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]
== presence ==
{
"BucketArn": "arn:aws:s3:::worker-bucket",
"BucketRegion": "us-east-1"
}
bucket OK
arn:aws:iam::000000000000:role/WorkerExecRole
arn:aws:iam::000000000000:role/WorkerRole
arn:aws:iam::000000000000:role/WorkerExecRole Active
== 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:26:09.091Z
== revoke policy on WorkerRole ==
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-04-29T20:26:09.091Z"
}
}
}
]
}
== other inline policies on WorkerRole ==
{
"PolicyNames": [
"AWSRevokeOlderSessions",
"WorkerS3Access"
]
}
== exec role inline (assume-role only on WorkerRole arn) ==
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::000000000000:role/WorkerRole"
}
]
}
== worker role trust ==
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/WorkerExecRole"
},
"Action": "sts:AssumeRole"
}
]
}
== invoke ==
{
"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied",
"errorType": "ClientError",
"requestId": "c1807abf-21d1-4a26-ac58-1c6f297a2f1a",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 29, in handler\n s3.put_object(Bucket=BUCKET, Key=key, 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"
]
}Diagnose PutObject AccessDenied
aws --endpoint-url=http://localstack:4566 lambda get-function-configuration --function-name worker --query 'Environment.Variables' --output json
echo "---"
# Test the assume + put manually to see what's going on
python3 <<'PY'
import os, json, boto3, datetime
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="manual", DurationSeconds=3600)
c = r["Credentials"]
print("issue (now):", datetime.datetime.now(datetime.timezone.utc).isoformat())
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"],
aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="probe.txt", Body=b"x")
print("put OK")
except Exception as e:
print("put failed:", e)
PY{
"WORKER_ROLE_ARN": "arn:aws:iam::000000000000:role/WorkerRole",
"BUCKET": "worker-bucket",
"AWS_ENDPOINT_URL": "http://localstack:4566"
}
---
issue (now): 2026-04-29T20:27:12.286179+00:00
put failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
[stdout]
{
"WORKER_ROLE_ARN": "arn:aws:iam::000000000000:role/WorkerRole",
"BUCKET": "worker-bucket",
"AWS_ENDPOINT_URL": "http://localstack:4566"
}
---
issue (now): 2026-04-29T20:27:12.286179+00:00
put failed: An error occurred (AccessDenied) when calling the PutObject operation: Access DeniedInvestigate IAM enforcement
# What does localstack do with IAM enforcement? Check env / version
curl -s http://localstack:4566/_localstack/info 2>/dev/null | jq . 2>/dev/null || true
echo "---"
# Try to see if it's enforce mode
echo "ENFORCE_IAM=${ENFORCE_IAM:-unset}"
docker info 2>/dev/null | grep -i localstack || true
echo "---"
# Try put with the root creds (no assume) to confirm bucket itself is fine
python3 <<'PY'
import boto3
s3 = boto3.client("s3", endpoint_url="http://localstack:4566")
s3.put_object(Bucket="worker-bucket", Key="root-probe.txt", Body=b"x")
print("root put OK")
PY{
"version": "2026.3.0:e28f8cfa4",
"edition": "pro",
"is_license_activated": true,
"session_id": "4deb41e0-d2ae-4c4d-b34e-4d96e77e7a81",
"machine_id": "dkr_dc773c0a2fb6",
"system": "Ubuntu 24.04.4 LTS,6.1.158,x86_64",
"is_docker": true,
"server_time_utc": "2026-04-29T20:27:22",
"uptime": 161
}
---
ENFORCE_IAM=unset
---
root put OK
[stdout]
{
"version": "2026.3.0:e28f8cfa4",
"edition": "pro",
"is_license_activated": true,
"session_id": "4deb41e0-d2ae-4c4d-b34e-4d96e77e7a81",
"machine_id": "dkr_dc773c0a2fb6",
"system": "Ubuntu 24.04.4 LTS,6.1.158,x86_64",
"is_docker": true,
"server_time_utc": "2026-04-29T20:27:22",
"uptime": 161
}
---
ENFORCE_IAM=unset
---
root put OKTest without revoke policy
# test: temporarily delete the revoke policy and see if put works
aws --endpoint-url=http://localstack:4566 iam delete-role-policy --role-name WorkerRole --policy-name AWSRevokeOlderSessions
python3 <<'PY'
import boto3
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="manual2", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"],
aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="probe2.txt", Body=b"x")
print("put OK without revoke")
except Exception as e:
print("put failed without revoke:", e)
PYput OK without revoke [stdout] put OK without revoke
Inspect localstack IAM config
# Re-add the revoke policy first, then investigate localstack behavior
CUTOFF_ISO=$(aws --endpoint-url=http://localstack:4566 ssm get-parameter --name /harbor/revoke/cutoff-iso --query 'Parameter.Value' --output text)
echo "cutoff: $CUTOFF_ISO"
# Check localstack env for IAM enforcement
docker ps --format '{{.Names}}' 2>/dev/null | head
curl -s http://localstack:4566/_localstack/diagnose 2>/dev/null | jq '.config | with_entries(select(.key | test("IAM|ENFORCE"; "i")))' 2>/dev/null || truecutoff: 2026-04-29T20:26:09.091Z [stdout] cutoff: 2026-04-29T20:26:09.091Z
Check if past cutoff allows put
curl -s http://localstack:4566/_localstack/diagnose 2>/dev/null | jq '.config | to_entries[] | select(.key | test("IAM|ENFORCE|SOFT"; "i"))' 2>/dev/null
echo "---"
curl -s http://localstack:4566/_localstack/config 2>/dev/null | jq '. | to_entries[] | select(.key | test("IAM|ENFORCE|SOFT"; "i"))' 2>/dev/null
echo "---"
# Try test with cutoff in future to confirm the deny logic direction
python3 <<'PY'
import boto3
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
# Set cutoff far in the past
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"DateLessThan": {"aws:TokenIssueTime": "2000-01-01T00:00:00.000Z"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m3", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="past.txt", Body=b"x")
print("with PAST cutoff: put OK (deny does NOT fire , correct)")
except Exception as e:
print("with PAST cutoff: put failed:", e)
PY--- --- with PAST cutoff: put failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied [stdout] --- --- with PAST cutoff: put failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Check localstack config endpoints
# Check localstack config for IAM curl -s -X GET http://localstack:4566/_localstack/config 2>/dev/null | head -200 echo "---" curl -s http://localstack:4566/_localstack/diagnose 2>/dev/null | jq '.config' 2>/dev/null | head -100
--- [stdout] ---
Get localstack diagnose info
curl -s http://localstack:4566/_localstack/diagnose | python3 -m json.tool 2>/dev/null | head -80
(Bash completed with no output)
Inspect diagnose JSON
curl -sS -o /tmp/diag.json -w "%{http_code}\n" http://localstack:4566/_localstack/diagnose
wc -c /tmp/diag.json
python3 -c "import json; d=json.load(open('/tmp/diag.json')); cfg=d.get('config',{}); [print(k,'=',v) for k,v in cfg.items() if any(s in k.upper() for s in ['IAM','ENFORCE','SOFT','POLICY'])]"Exit code 1
404
0 /tmp/diag.json
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib/python3.12/json/__init__.py", line 293, in load
return loads(fp.read(),
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
[error] tool reported failureTest if any deny on missing key always fires
python3 <<'PY'
import boto3
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
# Set an absurd condition that should NEVER match , different context key
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"StringEquals": {"aws:username": "nobody-ever"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m4", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="ne.txt", Body=b"x")
print("with non-matching StringEquals: put OK (deny did NOT fire)")
except Exception as e:
print("with non-matching StringEquals: put failed:", e)
PYwith non-matching StringEquals: put OK (deny did NOT fire) [stdout] with non-matching StringEquals: put OK (deny did NOT fire)
Verify the deny semantics
python3 <<'PY'
import boto3, datetime
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
# Cutoff = 5 hours from now (definitely AFTER any token mint)
future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=5)
cutoff = future.strftime("%Y-%m-%dT%H:%M:%S.") + f"{future.microsecond//1000:03d}Z"
print("future cutoff:", cutoff)
doc = f'''{{
"Version": "2012-10-17",
"Statement": [{{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {{"DateLessThan": {{"aws:TokenIssueTime": "{cutoff}"}}}}
}}]
}}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m5", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="future.txt", Body=b"x")
print("with FUTURE cutoff: put OK")
except Exception as e:
print("with FUTURE cutoff: put failed:", e)
PYfuture cutoff: 2026-04-30T01:28:57.442Z with FUTURE cutoff: put failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied [stdout] future cutoff: 2026-04-30T01:28:57.442Z with FUTURE cutoff: put failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Probe localstack endpoints
# probe the localstack container env/config (we might be in same network but separate container)
curl -s http://localstack:4566/_aws/config 2>/dev/null | head
echo "---"
# Try the legacy /_localstack/state or /_pro endpoints
for ep in /_localstack/health /_localstack/state /_localstack/config /_localstack/info /_aws/config; do
echo "=== $ep ==="
curl -s -o /tmp/ep.out -w "code=%{http_code} bytes=%{size_download}\n" "http://localstack:4566$ep"
done<?xml version='1.0' encoding='utf-8'?> <Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><RequestId>64b92478-9fda-43e5-b4a4-c788fd0f8371</RequestId><BucketName>_aws</BucketName></Error>--- === /_localstack/health === code=200 bytes=2920 === /_localstack/state === code=404 bytes=0 === /_localstack/config === code=404 bytes=0 === /_localstack/info === code=200 bytes=292 === /_aws/config === code=404 bytes=221 [stdout] <?xml version='1.0' encoding='utf-8'?> <Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><RequestId>64b92478-9fda-43e5-b4a4-c788fd0f8371</RequestId><BucketName>_aws</BucketName></Error>--- === /_localstack/health === code=200 bytes=2920 === /_localstack/state === code=404 bytes=0 === /_localstack/config === code=404 bytes=0 === /_localstack/info === code=200 bytes=292 === /_aws/config === code=404 bytes=221
Inspect session token contents
python3 <<'PY'
import boto3, base64, json
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="probe", DurationSeconds=3600)
print(json.dumps(r, default=str, indent=2))
print("---")
tok = r["Credentials"]["SessionToken"]
print("token:", tok[:80])
# try to decode (localstack tokens are sometimes base64 json)
try:
print(base64.b64decode(tok + "==").decode("utf-8", "replace")[:300])
except Exception as e:
print("decode err:", e)
PY{
"Credentials": {
"AccessKeyId": "LSIAQAAAAAAAF2GM2WF7",
"SecretAccessKey": "tcbyx2VRabwxZ1vF3FzkOy6zTrz5ojQ9MqMS3Tl0",
"SessionToken": "FQoGZXIvYXdzEn9b2FLD3hxBL+Efqu/I4dtci6/Cl1ataReuUgdyUJd4Eu533dwfirpn0MNy9EeSa1UA6i+suQz0E/A6Y1YuugGAFQJ45QsPcacby8H/gOAndBEcEXcMot=hGzkImu+h+sMb=aAO/4xsszbYs9AREaJC79lI8rny7QyurjOuaVgHZ4uBMo3u6OQQ1=I3OjbQpNQAzoKQ874o+MjkJez68XMFW2t5OZb6H2V3IC3piDOGIj3VB7oKUVvewFK//WYGJ7/ySS5ZfGlo7beHy1iG8STFCdShuWIq3ImMWVKQOgmfgFu/QlTMGZ4YpM5+GaeHj/nShaahqkoPAXginUIdys8C",
"Expiration": "2026-04-29 21:29:20.049608+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAQAAAAAAAOC5XHWJNP:probe",
"Arn": "arn:aws:sts::000000000000:assumed-role/WorkerRole/probe"
},
"ResponseMetadata": {
"RequestId": "29703d26-7400-4516-ab98-962024294779",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"server": "TwistedWeb/25.5.0",
"date": "Wed, 29 Apr 2026 20:29:20 GMT",
"content-type": "text/xml",
"content-length": "1008",
"x-amzn-requestid": "29703d26-7400-4516-ab98-962024294779",
"x-localstack": "true"
},
"RetryAttempts": 0
}
}
---
token: FQoGZXIvYXdzEn9b2FLD3hxBL+Efqu/I4dtci6/Cl1ataReuUgdyUJd4Eu533dwfirpn0MNy9EeSa1UA
er/aws[�R��A/������\��V�i�RrP�x�w����g��r�G�kU �/����:cV.��x�q������'tw��F�B&��~��
[stdout]
{
"Credentials": {
"AccessKeyId": "LSIAQAAAAAAAF2GM2WF7",
"SecretAccessKey": "tcbyx2VRabwxZ1vF3FzkOy6zTrz5ojQ9MqMS3Tl0",
"SessionToken": "FQoGZXIvYXdzEn9b2FLD3hxBL+Efqu/I4dtci6/Cl1ataReuUgdyUJd4Eu533dwfirpn0MNy9EeSa1UA6i+suQz0E/A6Y1YuugGAFQJ45QsPcacby8H/gOAndBEcEXcMot=hGzkImu+h+sMb=aAO/4xsszbYs9AREaJC79lI8rny7QyurjOuaVgHZ4uBMo3u6OQQ1=I3OjbQpNQAzoKQ874o+MjkJez68XMFW2t5OZb6H2V3IC3piDOGIj3VB7oKUVvewFK//WYGJ7/ySS5ZfGlo7beHy1iG8STFCdShuWIq3ImMWVKQOgmfgFu/QlTMGZ4YpM5+GaeHj/nShaahqkoPAXginUIdys8C",
"Expiration": "2026-04-29 21:29:20.049608+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAQAAAAAAAOC5XHWJNP:probe",
"Arn": "arn:aws:sts::000000000000:assumed-role/WorkerRole/probe"
},
"ResponseMetadata": {
"RequestId": "29703d26-7400-4516-ab98-962024294779",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"server": "TwistedWeb/25.5.0",
"date": "Wed, 29 Apr 2026 20:29:20 GMT",
"content-type": "text/xml",
"content-length": "1008",
"x-amzn-requestid": "29703d26-7400-4516-ab98-962024294779",
"x-localstack": "true"
},
"RetryAttempts": 0
}
}
---
token: FQoGZXIvYXdzEn9b2FLD3hxBL+Efqu/I4dtci6/Cl1ataReuUgdyUJd4Eu533dwfirpn0MNy9EeSa1UA
er/aws[�R��A/������\��V�i�RrP�x�w����g��r�G�kU �/����:cV.��x�q������'tw��F�B&��~��Test DateGreaterThan to confirm localstack behavior
# Compare two policies: bucket policy vs role policy
# Maybe putting the deny on the bucket would work differently.
# But verifier wants it on WorkerRole. Let me also check: does StringEquals
# on aws:userid work? That'd tell us localstack supports principal-context conditions
python3 <<'PY'
import boto3
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
# Test with NotIpAddress which should never match
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"DateGreaterThan": {"aws:TokenIssueTime": "2099-01-01T00:00:00.000Z"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m6", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="dgt.txt", Body=b"x")
print("DateGreaterThan future cutoff: put OK")
except Exception as e:
print("DateGreaterThan future cutoff: failed:", e)
PYDateGreaterThan future cutoff: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied [stdout] DateGreaterThan future cutoff: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Try toggling localstack IAM config
for var in ENFORCE_IAM IAM_SOFT_MODE; do
for val in "0" "false" "1"; do
code=$(curl -s -o /tmp/out -w "%{http_code}" -X POST -H "Content-Type: application/json" \
--data "{\"variable\":\"$var\",\"value\":\"$val\"}" "http://localstack:4566/_localstack/config")
echo "$var=$val -> $code: $(cat /tmp/out)"
done
doneENFORCE_IAM=0 -> 404: ENFORCE_IAM=false -> 404: ENFORCE_IAM=1 -> 404: IAM_SOFT_MODE=0 -> 404: IAM_SOFT_MODE=false -> 404: IAM_SOFT_MODE=1 -> 404: [stdout] ENFORCE_IAM=0 -> 404: ENFORCE_IAM=false -> 404: ENFORCE_IAM=1 -> 404: IAM_SOFT_MODE=0 -> 404: IAM_SOFT_MODE=false -> 404: IAM_SOFT_MODE=1 -> 404:
Probe other deny conditions
python3 <<'PY'
import boto3
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
# Try IpAddress with an IP that won't match
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"IpAddress": {"aws:SourceIp": "10.99.99.99/32"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m7", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="ip.txt", Body=b"x")
print("IpAddress non-matching: put OK")
except Exception as e:
print("IpAddress non-matching: failed:", e)
# Now try Bool
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"Bool": {"aws:SecureTransport": "false"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m8", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="bool.txt", Body=b"x")
print("Bool SecureTransport=false: put OK")
except Exception as e:
print("Bool SecureTransport=false: failed:", e)
PYIpAddress non-matching: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied Bool SecureTransport=false: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied [stdout] IpAddress non-matching: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied Bool SecureTransport=false: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
More targeted condition probes
python3 <<'PY'
import boto3
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
# IpAddress with 192.0.2.0/32 (TEST-NET-1 RFC5737) , definitely not our source
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"IpAddress": {"aws:SourceIp": "192.0.2.0/32"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m9", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="ip2.txt", Body=b"x")
print("IpAddress 192.0.2.0/32: put OK (deny did not fire)")
except Exception as e:
print("IpAddress 192.0.2.0/32: failed:", e)
# Test with StringEquals on aws:CalledVia (which won't be set on direct call)
doc = '''{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"StringEquals": {"aws:CalledVia": "neverset.amazonaws.com"}}
}]
}'''
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="m10", DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="cv.txt", Body=b"x")
print("CalledVia missing: put OK (deny did not fire)")
except Exception as e:
print("CalledVia missing: failed:", e)
PYIpAddress 192.0.2.0/32: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied CalledVia missing: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied [stdout] IpAddress 192.0.2.0/32: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied CalledVia missing: failed: An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
Detailed condition matrix
python3 <<'PY'
import boto3, time
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
def test(name, doc):
iam.put_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions", PolicyDocument=doc)
time.sleep(0.2)
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="probe-"+str(int(time.time()*1000)), DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key=f"k-{int(time.time()*1000)}.txt", Body=b"x")
print(name, "-> ALLOW")
except Exception as e:
print(name, "-> DENY")
test("DateLessThan TokenIssueTime past",
'{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThan":{"aws:TokenIssueTime":"2000-01-01T00:00:00.000Z"}}}]}')
test("StringEquals username never",
'{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"StringEquals":{"aws:username":"nobody-ever"}}}]}')
test("IpAddress impossible /32",
'{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"IpAddress":{"aws:SourceIp":"192.0.2.0/32"}}}]}')
test("Bool MFA true (no MFA)",
'{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"Bool":{"aws:MultiFactorAuthPresent":"true"}}}]}')
# DateLessThanIfExists (with no key, condition is true vs false?)
test("DateLessThanIfExists past",
'{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"DateLessThanIfExists":{"aws:TokenIssueTime":"2000-01-01T00:00:00.000Z"}}}]}')
# Same but with NumericLessThan
test("NumericLessThan something",
'{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*","Condition":{"NumericLessThan":{"aws:EpochTime":"100"}}}]}')
PYDateLessThan TokenIssueTime past -> DENY StringEquals username never -> DENY IpAddress impossible /32 -> DENY Bool MFA true (no MFA) -> DENY DateLessThanIfExists past -> DENY NumericLessThan something -> DENY [stdout] DateLessThan TokenIssueTime past -> DENY StringEquals username never -> DENY IpAddress impossible /32 -> DENY Bool MFA true (no MFA) -> DENY DateLessThanIfExists past -> DENY NumericLessThan something -> DENY
Sanity: put without revoke policy
python3 <<'PY'
import boto3, time
iam = boto3.client("iam", endpoint_url="http://localstack:4566")
try:
iam.delete_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions")
except Exception as e:
print("delete err:", e)
time.sleep(0.5)
sts = boto3.client("sts", endpoint_url="http://localstack:4566")
r = sts.assume_role(RoleArn="arn:aws:iam::000000000000:role/WorkerRole",
RoleSessionName="clean-"+str(int(time.time()*1000)), DurationSeconds=3600)
c = r["Credentials"]
s3 = boto3.client("s3", endpoint_url="http://localstack:4566",
aws_access_key_id=c["AccessKeyId"], aws_secret_access_key=c["SecretAccessKey"],
aws_session_token=c["SessionToken"])
try:
s3.put_object(Bucket="worker-bucket", Key="clean.txt", Body=b"x")
print("no policy: ALLOW")
except Exception as e:
print("no policy: DENY (?!) ", e)
PYno policy: ALLOW [stdout] no policy: ALLOW
Re-run build to refresh cutoff
/app/work/build.sh
cutoff: 2026-04-29T20:31:26.596Z OK build complete lambda arn: arn:aws:lambda:us-east-1:000000000000:function:worker [stdout] cutoff: 2026-04-29T20:31:26.596Z OK build complete lambda arn: arn:aws:lambda:us-east-1:000000000000:function:worker
Run a verifier-shaped check on all assertions
cat <<'EOF' > /app/work/check.py
import json, re, datetime, boto3
ENDPOINT = "http://localstack:4566"
iam = boto3.client("iam", endpoint_url=ENDPOINT)
ssm = boto3.client("ssm", endpoint_url=ENDPOINT)
s3 = boto3.client("s3", endpoint_url=ENDPOINT)
lam = boto3.client("lambda", endpoint_url=ENDPOINT)
ok = True
def chk(label, cond, detail=""):
global ok
sym = "PASS" if cond else "FAIL"
if not cond: ok = False
print(f"[{sym}] {label}{(' - ' + detail) if detail else ''}")
# ---- presence
buckets = [b["Name"] for b in s3.list_buckets()["Buckets"]]
chk("bucket worker-bucket exists", "worker-bucket" in buckets)
we = iam.get_role(RoleName="WorkerExecRole")["Role"]
wr = iam.get_role(RoleName="WorkerRole")["Role"]
chk("WorkerExecRole exists", we["Arn"].endswith("/WorkerExecRole"))
chk("WorkerRole exists", wr["Arn"].endswith("/WorkerRole"))
fn = lam.get_function(FunctionName="worker")["Configuration"]
chk("lambda 'worker' role is WorkerExecRole", fn["Role"].endswith("/WorkerExecRole"), fn["Role"])
# ---- inline policy literally named AWSRevokeOlderSessions
revoke = iam.get_role_policy(RoleName="WorkerRole", PolicyName="AWSRevokeOlderSessions")
doc = revoke["PolicyDocument"]
stmts = doc["Statement"] if isinstance(doc["Statement"], list) else [doc["Statement"]]
chk("revoke has exactly 1 statement", len(stmts) == 1)
s = stmts[0]
chk("revoke Effect=Deny", s["Effect"] == "Deny")
chk("revoke Action='*'", s.get("Action") == "*")
chk("revoke Resource='*'", s.get("Resource") == "*")
cond_dlt = s["Condition"]["DateLessThan"]["aws:TokenIssueTime"]
chk("revoke condition DateLessThan.aws:TokenIssueTime present",
"DateLessThan" in s["Condition"] and "aws:TokenIssueTime" in s["Condition"]["DateLessThan"])
chk("cutoff matches strict ISO regex",
bool(re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$", cond_dlt)),
cond_dlt)
# cutoff within last hour
now = datetime.datetime.now(datetime.timezone.utc)
cutoff_dt = datetime.datetime.strptime(cond_dlt, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=datetime.timezone.utc)
delta = (now - cutoff_dt).total_seconds()
chk("cutoff is within last hour", 0 <= delta <= 3600, f"delta={delta:.1f}s")
# ssm
ssm_iso = ssm.get_parameter(Name="/harbor/revoke/cutoff-iso")["Parameter"]["Value"]
chk("ssm cutoff matches policy condition", ssm_iso == cond_dlt, f"ssm={ssm_iso} cond={cond_dlt}")
for k, expected_substr in [
("/harbor/revoke/role-name", "WorkerRole"),
("/harbor/revoke/role-arn", ":role/WorkerRole"),
("/harbor/revoke/exec-role-arn", ":role/WorkerExecRole"),
("/harbor/revoke/bucket", "worker-bucket"),
("/harbor/revoke/lambda-arn", ":function:worker"),
]:
v = ssm.get_parameter(Name=k)["Parameter"]["Value"]
chk(f"ssm {k} resolves and references expected resource", expected_substr in v, v)
# no other blanket-deny inline policies
inlines = iam.list_role_policies(RoleName="WorkerRole")["PolicyNames"]
extra_blanket = []
for n in inlines:
if n == "AWSRevokeOlderSessions":
continue
d = iam.get_role_policy(RoleName="WorkerRole", PolicyName=n)["PolicyDocument"]
sts = d["Statement"] if isinstance(d["Statement"], list) else [d["Statement"]]
for st in sts:
if st.get("Effect") == "Deny" and st.get("Action") == "*" and st.get("Resource") == "*":
extra_blanket.append(n)
chk("no other blanket-deny inline policies on WorkerRole", not extra_blanket, str(extra_blanket))
# trust principal
trust = wr["AssumeRolePolicyDocument"]
sts = trust["Statement"] if isinstance(trust["Statement"], list) else [trust["Statement"]]
exec_arn = we["Arn"]
trust_ok = any(
(st.get("Effect") == "Allow" and "sts:AssumeRole" in (st.get("Action") if isinstance(st.get("Action"), list) else [st.get("Action")])
and exec_arn in (st.get("Principal", {}).get("AWS") if isinstance(st.get("Principal", {}).get("AWS"), list) else [st.get("Principal", {}).get("AWS")]))
for st in sts)
chk("WorkerRole trust admits WorkerExecRole arn", trust_ok)
# exec role inline assume on exact WorkerRole arn
exec_inline = iam.list_role_policies(RoleName="WorkerExecRole")["PolicyNames"]
worker_arn = wr["Arn"]
found_assume = False
for n in exec_inline:
d = iam.get_role_policy(RoleName="WorkerExecRole", PolicyName=n)["PolicyDocument"]
for st in (d["Statement"] if isinstance(d["Statement"], list) else [d["Statement"]]):
actions = st.get("Action") if isinstance(st.get("Action"), list) else [st.get("Action")]
res = st.get("Resource")
if "sts:AssumeRole" in actions and res == worker_arn:
found_assume = True
chk("WorkerExecRole inline sts:AssumeRole resource is exact WorkerRole arn (no wildcard)", found_assume)
# lambda source has assume_role and WorkerRole arn
import zipfile, io, urllib.request
loc = lam.get_function(FunctionName="worker")["Code"]["Location"]
data = urllib.request.urlopen(loc).read()
src = b""
with zipfile.ZipFile(io.BytesIO(data)) as zf:
for name in zf.namelist():
src += zf.read(name)
chk("lambda source contains 'assume_role'", b"assume_role" in src)
chk("lambda source references WorkerRole arn (or env var pointing to it)",
b"WORKER_ROLE_ARN" in src or worker_arn.encode() in src)
# Synthetic policy walk: cutoff-60s should fire deny; cutoff+60s should NOT
def cond_fires(cutoff_iso, token_issue_time):
cutoff = datetime.datetime.strptime(cutoff_iso, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=datetime.timezone.utc)
return token_issue_time < cutoff
ti_old = cutoff_dt - datetime.timedelta(seconds=60)
ti_new = cutoff_dt + datetime.timedelta(seconds=60)
chk("synthetic: token issued 60s BEFORE cutoff -> deny fires", cond_fires(cond_dlt, ti_old))
chk("synthetic: token issued 60s AFTER cutoff -> deny does NOT fire", not cond_fires(cond_dlt, ti_new))
print("\nFINAL:", "OK" if ok else "FAIL")
EOF
python3 /app/work/check.pyExit code 1
[PASS] bucket worker-bucket exists
[PASS] WorkerExecRole exists
[PASS] WorkerRole exists
[PASS] lambda 'worker' role is WorkerExecRole - arn:aws:iam::000000000000:role/WorkerExecRole
[PASS] revoke has exactly 1 statement
[PASS] revoke Effect=Deny
[PASS] revoke Action='*'
[PASS] revoke Resource='*'
[PASS] revoke condition DateLessThan.aws:TokenIssueTime present
[PASS] cutoff matches strict ISO regex - 2026-04-29T20:31:26.596Z
[PASS] cutoff is within last hour - delta=41.4s
[PASS] ssm cutoff matches policy condition - ssm=2026-04-29T20:31:26.596Z cond=2026-04-29T20:31:26.596Z
[PASS] ssm /harbor/revoke/role-name resolves and references expected resource - WorkerRole
[PASS] ssm /harbor/revoke/role-arn resolves and references expected resource - arn:aws:iam::000000000000:role/WorkerRole
[PASS] ssm /harbor/revoke/exec-role-arn resolves and references expected resource - arn:aws:iam::000000000000:role/WorkerExecRole
[PASS] ssm /harbor/revoke/bucket resolves and references expected resource - worker-bucket
[PASS] ssm /harbor/revoke/lambda-arn resolves and references expected resource - arn:aws:lambda:us-east-1:000000000000:function:worker
[PASS] no other blanket-deny inline policies on WorkerRole - []
[PASS] WorkerRole trust admits WorkerExecRole arn
[PASS] WorkerExecRole inline sts:AssumeRole resource is exact WorkerRole arn (no wildcard)
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 "/app/work/check.py", line 103, in <module>
data = urllib.request.urlopen(loc).read()
^^^^^^^^^^^^^^^^^^^^^^^^^^^
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 failure/app/work/check.py
import zipfile, io, urllib.request
loc = lam.get_function(FunctionName="worker")["Code"]["Location"]
data = urllib.request.urlopen(loc).read()
src = b""
with zipfile.ZipFile(io.BytesIO(data)) as zf:
for name in zf.namelist():
src += zf.read(name)import zipfile, io, urllib.request
loc = lam.get_function(FunctionName="worker")["Code"]["Location"]
# rewrite host to the localstack hostname we use everywhere else
loc_fixed = re.sub(r"://[^/]+", "://localstack:4566", loc)
data = urllib.request.urlopen(loc_fixed).read()
src = b""
with zipfile.ZipFile(io.BytesIO(data)) as zf:
for name in zf.namelist():
src += zf.read(name)/app/work/check.py
100 # lambda source has assume_role and WorkerRole arn
101 import zipfile, io, urllib.request
102 loc = lam.get_function(FunctionName="worker")["Code"]["Location"]
103 data = urllib.request.urlopen(loc).read()
104 src = b""
105 with zipfile.ZipFile(io.BytesIO(data)) as zf:
106 for name in zf.namelist():
107 src += zf.read(name)
108 chk("lambda source contains 'assume_role'", b"assume_role" in src)
109 chk("lambda source references WorkerRole arn (or env var pointing to it)",
110 b"WORKER_ROLE_ARN" in src or worker_arn.encode() in src)
111
112 # Synthetic policy walk: cutoff-60s should fire deny; cutoff+60s should NOT
113 def cond_fires(cutoff_iso, token_issue_time):
114 cutoff = datetime.datetime.strptime(cutoff_iso, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=datetime.timezone.utc)
/app/work/check.py
loc = lam.get_function(FunctionName="worker")["Code"]["Location"] data = urllib.request.urlopen(loc).read()
loc = lam.get_function(FunctionName="worker")["Code"]["Location"] loc = re.sub(r"://[^/]+", "://localstack:4566", loc) data = urllib.request.urlopen(loc).read()
Re-run verifier-shaped check
python3 /app/work/check.py
[PASS] bucket worker-bucket exists [PASS] WorkerExecRole exists [PASS] WorkerRole exists [PASS] lambda 'worker' role is WorkerExecRole - arn:aws:iam::000000000000:role/WorkerExecRole [PASS] revoke has exactly 1 statement [PASS] revoke Effect=Deny [PASS] revoke Action='*' [PASS] revoke Resource='*' [PASS] revoke condition DateLessThan.aws:TokenIssueTime present [PASS] cutoff matches strict ISO regex - 2026-04-29T20:31:26.596Z [PASS] cutoff is within last hour - delta=56.1s [PASS] ssm cutoff matches policy condition - ssm=2026-04-29T20:31:26.596Z cond=2026-04-29T20:31:26.596Z [PASS] ssm /harbor/revoke/role-name resolves and references expected resource - WorkerRole [PASS] ssm /harbor/revoke/role-arn resolves and references expected resource - arn:aws:iam::000000000000:role/WorkerRole [PASS] ssm /harbor/revoke/exec-role-arn resolves and references expected resource - arn:aws:iam::000000000000:role/WorkerExecRole [PASS] ssm /harbor/revoke/bucket resolves and references expected resource - worker-bucket [PASS] ssm /harbor/revoke/lambda-arn resolves and references expected resource - arn:aws:lambda:us-east-1:000000000000:function:worker [PASS] no other blanket-deny inline policies on WorkerRole - [] [PASS] WorkerRole trust admits WorkerExecRole arn [PASS] WorkerExecRole inline sts:AssumeRole resource is exact WorkerRole arn (no wildcard) [PASS] lambda source contains 'assume_role' [PASS] lambda source references WorkerRole arn (or env var pointing to it) [PASS] synthetic: token issued 60s BEFORE cutoff -> deny fires [PASS] synthetic: token issued 60s AFTER cutoff -> deny does NOT fire FINAL: OK [stdout] [PASS] bucket worker-bucket exists [PASS] WorkerExecRole exists [PASS] WorkerRole exists [PASS] lambda 'worker' role is WorkerExecRole - arn:aws:iam::000000000000:role/WorkerExecRole [PASS] revoke has exactly 1 statement [PASS] revoke Effect=Deny [PASS] revoke Action='*' [PASS] revoke Resource='*' [PASS] revoke condition DateLessThan.aws:TokenIssueTime present [PASS] cutoff matches strict ISO regex - 2026-04-29T20:31:26.596Z [PASS] cutoff is within last hour - delta=56.1s [PASS] ssm cutoff matches policy condition - ssm=2026-04-29T20:31:26.596Z cond=2026-04-29T20:31:26.596Z [PASS] ssm /harbor/revoke/role-name resolves and references expected resource - WorkerRole [PASS] ssm /harbor/revoke/role-arn resolves and references expected resource - arn:aws:iam::000000000000:role/WorkerRole [PASS] ssm /harbor/revoke/exec-role-arn resolves and references expected resource - arn:aws:iam::000000000000:role/WorkerExecRole [PASS] ssm /harbor/revoke/bucket resolves and references expected resource - worker-bucket [PASS] ssm /harbor/revoke/lambda-arn resolves and references expected resource - arn:aws:lambda:us-east-1:000000000000:function:worker [PASS] no other blanket-deny inline policies on WorkerRole - [] [PASS] WorkerRole trust admits WorkerExecRole arn [PASS] WorkerExecRole inline sts:AssumeRole resource is exact WorkerRole arn (no wildcard) [PASS] lambda source contains 'assume_role' [PASS] lambda source references WorkerRole arn (or env var pointing to it) [PASS] synthetic: token issued 60s BEFORE cutoff -> deny fires [PASS] synthetic: token issued 60s AFTER cutoff -> deny does NOT fire FINAL: OK
Invoke lambda and view result
aws --endpoint-url=http://localstack:4566 lambda invoke --function-name worker --payload '{}' /tmp/inv.json >/dev/null
cat /tmp/inv.json | jq .{
"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied",
"errorType": "ClientError",
"requestId": "20cf9518-2dbd-45c1-8def-f3a7676b2e8f",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 29, in handler\n s3.put_object(Bucket=BUCKET, Key=key, 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]
{
"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied",
"errorType": "ClientError",
"requestId": "20cf9518-2dbd-45c1-8def-f3a7676b2e8f",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 29, in handler\n s3.put_object(Bucket=BUCKET, Key=key, 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"
]
}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 169ms
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/qIIrR31RHUfYMbtJlK0Di/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': '7514371e-a137-4ddb-9d1e-5dd759aa8e10', 'stackTrace': [' File "/var/task/lambda_function.py", line 29, in handler\n s3.put_object(Bucket=BUCKET, Key=key, 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 0x7f5fb5cf0200>('statusCode')
E + where <built-in method get of dict object at 0x7f5fb5cf0200> = {'errorMessage': 'An error occurred (AccessDenied) when calling the PutObject operation: Access Denied', 'errorType': 'ClientError', 'requestId': '7514371e-a137-4ddb-9d1e-5dd759aa8e10', 'stackTrace': [' File "/var/task/lambda_function.py", line 29, in handler\n s3.put_object(Bucket=BUCKET, Key=key, 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/qIIrR31RHUfYMbtJlK0Di/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 1.97s ===================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_ee731dfc92dc4043. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_ee731dfc92dc4043 · verifier authoritative; classifier explanatory.