What a policy looks like
"Give Ben read access" is too vague for a computer to enforce: which files, which actions, forever or until when? IAM needs permissions written in an exact, checkable form, and that form is what you will actually read at work. The first step in debugging almost every AccessDenied error is opening a policy document.
A policy is that document: JSON attached to an identity. It is a list of statements, and each statement has three core parts:
- Effect:
AlloworDeny - Action: what API calls, like
s3:GetObject(read a file from S3) - Resource: what things, named by ARN (Amazon Resource Name), a globally unique ID like
arn:aws:s3:::my-app-photos/*
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-photos/*"
}]
}Read it aloud: this identity may read and write objects, but only inside the my-app-photos bucket. That is least privilege from lesson 2-1 written down.
The three evaluation rules
When a request arrives, IAM gathers every policy attached to the identity and applies three rules, in order:
- Default deny. With no policies at all, the answer is no.
- An explicit
Denyalways wins. If any statement denies the action, it is denied even if ten others allow it. - Otherwise, one
Allowis enough.
That is the whole algorithm. Explicit denies exist as guardrails: a company can attach deny deleting databases in production to everyone, and no allow anywhere can override it.
IAM's evaluation algorithm in miniature
This function is the three-rule algorithm written out. It takes two answers, whether there is an explicit deny and whether there is an explicit allow, then decides.
decide() { local explicit_deny=$1 explicit_allow=$2 if [ "$explicit_deny" = "yes" ]; then echo "DENY (an explicit deny always wins)" elif [ "$explicit_allow" = "yes" ]; then echo "ALLOW" else echo "DENY (default: nothing allowed it)" fi } decide no yes decide yes yes decide no no
Output
ALLOW DENY (an explicit deny always wins) DENY (default: nothing allowed it)
Check the three rules against those three lines. The first call has an allow and no deny, so it is allowed. The second has both, and the deny branch fires first, which is rule 2. The third has neither, and falls through to default deny, which is rule 1.
The order of the if branches is the algorithm. Deny is tested before allow, which is exactly why an explicit deny cannot be overridden by any number of allows.
Three requests through the same algorithm
Ben requests s3:DeleteObject in three situations, and decide is called once for each.
decide() { local explicit_deny=$1 explicit_allow=$2 if [ "$explicit_deny" = "yes" ]; then echo "DENY (an explicit deny always wins)" elif [ "$explicit_allow" = "yes" ]; then echo "ALLOW" else echo "DENY (default: nothing allowed it)" fi } decide no no decide yes no decide no yes
Output
DENY (default: nothing allowed it) DENY (an explicit deny always wins) ALLOW
Mapping each call to its situation
- Ben has no policies at all, so both arguments are
noand default deny applies. This is the state of every brand-new IAM user. - A policy allows
s3:*while a company guardrail explicitly deniesDeleteObject. Thes3:*wildcard does coverDeleteObject, but rule 2 makes the allow irrelevant, so the call passesyesfor the deny. - A policy explicitly allows
DeleteObjectand nothing denies it, so the second branch fires and the request is allowed.
A broad allow against a narrow deny
Policy A allows s3:* on every bucket, and policy B explicitly denies s3:DeleteObject on arn:aws:s3:::prod-data/*. Deleting prod-data/backup.zip is denied, because an explicit deny beats any number of allows.
This is rule 2 from this lesson. Explicit deny wins no matter what else is attached, no matter how broad the allow is, and no matter which order the policies were attached in. IAM gathers all of them and evaluates the set, so there is no notion of a later policy overriding an earlier one.
The narrow scope of policy B is what makes this useful in practice. The same identity can still delete objects in every other bucket, since the deny names only prod-data/*. That is exactly how teams protect production data while leaving broad permissions in place everywhere else.
Reading a bucket name out of an ARN
For a Resource line of arn:aws:s3:::acme-billing-exports/*, the bucket is acme-billing-exports.
S3 ARNs read arn:aws:s3:::bucket-name/path, so this statement covers every file in that one bucket and nothing anywhere else.
Two parsing details make the answer findable. In an S3 ARN the bucket name comes right after the last colon, and the empty fields before it are where region and account ID would go for other services. The /* at the end means "every file inside it", so it is not part of the bucket's name.
Scoping Resource this tightly is least privilege from lesson 2-1 in its written form. Even if the Action list is generous, the blast radius stays one bucket, which is the single most effective habit in policy writing.