AccessDenied on S3 from an EC2 instance
aws s3 ls s3://<BUCKET>/ fails with AccessDenied on the instance
Focused incident mode — strips away everything but this runbook.
Symptoms
- aws s3 ls s3://<BUCKET>/ fails with AccessDenied on the instance
- App that earlier read S3 suddenly gets 403
- Works from your laptop/CI but fails from the instance role
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Caller identity on the instance
aws sts get-caller-identityAttached role policies
aws iam list-attached-role-policies --role-name <ROLE-NAME>
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- No policy granting s3:GetObject/ListBucket on <BUCKET>
- Error appears after a policy rewrite
Confirm before fixing
aws iam get-role-policy --role-name <ROLE-NAME> --policy-name <POLICY-NAME>Inspect the inline policy
Fix
aws iam put-role-policy --role-name <ROLE-NAME> --policy-name s3-read --policy-document file://s3-access.jsonAttach a policy permitting the required actions on arn:aws:s3:::<BUCKET>/*
Expected: policy attached
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
aws sts get-caller-identityIdentify the active role
aws s3api head-object --bucket <BUCKET> --key <KEY> --debug 2>&1 | grep -i deniedCapture the exact denied action
aws iam put-role-policy --role-name <ROLE-NAME> --policy-name s3-access --policy-document file://s3-access.jsonGrant the needed s3 actions scoped to the bucket
aws s3api head-object --bucket <BUCKET> --key <KEY>Re-test until it succeeds
Prevention
- Use IAM condition keys like s3:ResourceAccount and glue/role specific policies per app.
- Adopt Infrastructure permissions boundaries so role policies and bucket policies agree.
- Run an automated IAM Access Analyzer for cross-account access before deploys.
Related runbooks
Same stack, same failure family — worth a skim.
Freshly created role fails with AccessDenied/not authorized
App or pipeline fails with 'not authorized to perform sts:AssumeRole'
A role in account A cannot read a bucket owned by account B
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.