You attached a policy with
s3:GetObject. You still get AccessDenied. Most people respond by adding broader permissions, which is the wrong fix. The real issue is usually that they don't know how AWS decides. This lab walks through the evaluation logic by creating each failure on purpose.Level: Beginner to intermediate Time: about 40 minutes Cost: effectively free (IAM, one small S3 bucket). Delete everything afterward.
The decision logic in four rules
- Everything is denied by default (implicit deny).
- An explicit Allow can override the implicit deny.
- An explicit Deny always wins, from any policy.
- Guardrails (permission boundaries, SCPs, session policies) limit what can be allowed. They never grant anything by themselves.
For a request to succeed, there must be an allow, and no explicit deny, and nothing in a guardrail layer cutting it off.
| Policy type | Grants permissions? | Acts as a limit? |
|---|---|---|
| Identity-based (user, group, role) | Yes | No |
| Resource-based (bucket policy, etc.) | Yes | No |
| Permission boundary | No | Yes, on one user or role |
| SCP (AWS Organizations) | No | Yes, on accounts |
| Session policy | No | Yes, on one session |
Lab setup
- Create a test S3 bucket and upload one small file.
- Create an IAM user (or an Identity Center permission set) with no permissions, and configure a CLI profile for it, named
lab. - Run:
bash
1
aws s3 ls s3://YOUR-BUCKET --profile labExpected result: AccessDenied. This is the implicit deny.
Experiment 1: add an allow
Attach a policy allowing
s3:ListBucket on the bucket ARN:json
1
2
3
4
5
6
7
8
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::YOUR-BUCKET"
}]
}Run the list command again. It works.
Now try downloading the file:
bash
1
aws s3 cp s3://YOUR-BUCKET/file.txt . --profile labExpected result: AccessDenied again. Listing and reading are different actions, and they target different resources:
ListBucket applies to the bucket, GetObject applies to objects (arn:aws:s3:::YOUR-BUCKET/*).Observation: this resource-ARN mismatch is one of the most common causes of "my allow isn't working."
Experiment 2: fix it with the correct resource
Add
s3:GetObject with the resource arn:aws:s3:::YOUR-BUCKET/*. The download now works.Experiment 3: an explicit deny beats the allow
Add a second statement denying
s3:GetObject on the same objects. Run the download again.Expected result: AccessDenied, even though the allow is still attached. Remove the deny to restore access.
Observation: this is why a stray deny (often in a group policy or bucket policy) is so hard to find. The allow looks perfect.
Experiment 4: a permission boundary limits the allow
- Create a managed policy allowing only
s3:ListBucket. - Set it as the user's permission boundary.
- Keep the identity policy that allows
GetObject. - Try the download.
Expected result: AccessDenied. Effective permissions are the intersection of the identity policy and the boundary.
Observation: boundaries are how you let people create roles without letting them exceed a ceiling. Many teams confuse this with granting.
How to debug instead of guess
- Read the error message. Newer AccessDenied messages often say what type of policy caused the denial, such as an explicit deny in an identity-based or resource-based policy, or a missing allow.
- Use the IAM Policy Simulator. Test an action against a principal and see which statement matched.
- Check CloudTrail. Find the denied event and read the
errorCodeanderrorMessagefields. - Check every layer. Identity policy, resource policy, boundary, SCP, and session policy.
One caveat: SCPs do not apply to the Organizations management account, and the policy simulator may not fully model every guardrail type in every scenario. Treat it as a strong hint, then confirm with a real call.
A short troubleshooting checklist
| Symptom | First thing to check |
|---|---|
| List works, download fails | Object-level resource ARN (/*) |
| Allow exists but still denied | An explicit deny in any attached policy |
| Admin-like role can't do something | Permission boundary or SCP |
| Works in console, fails in CLI | Different principal or session policy |
| Cross-account access fails | Both sides must allow it |
Clean up
Delete the IAM user and policies, remove the CLI profile and any access keys, empty and delete the bucket.
Key takeaways
- Default is deny. Explicit deny always wins.
- Boundaries and SCPs limit, they do not grant.
- The action and resource ARN must match, so object actions need
/*. - Debug with evidence: error text, Policy Simulator, and CloudTrail.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article