How IAM Actually Decides Who Gets In
IAM feels random until you see the one check it runs on every request. After that, policies stop being magic words.
- #aws
- #iam
- #security
My first week with IAM went like everyone else’s. I wrote a policy, it didn’t work, so I made it broader. Still broken. Made it broader again and it finally worked, except now it was wide enough that a typo in a Lambda could have wiped production. I hadn’t learned IAM. I’d just kept loosening it until it stopped complaining.
The thing nobody told me is that IAM isn’t random. It runs the same check on every request. Once you can see that check, policies stop feeling like magic words.
Every request goes on trial
When anything calls an AWS API, IAM makes a quick decision with three rules, in order.
First, everything starts denied. No policy, no access.
Second, an explicit Allow is the only thing that grants access.
Third, an explicit Deny wins over everything. If any policy denies the request, it’s done. No Allow anywhere brings it back.
So a request goes through only when something allows it and nothing denies it. Read every policy with that in mind and most of the confusion goes away.
A policy answers four questions
A statement is really just four answers. Read them in this order. Who is asking (the principal, usually set by where the policy is attached). What they want to do (the action, like s3:GetObject). Which thing they want to do it to (the resource, an ARN). And under what conditions (from this IP, with MFA, before this date).
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::reports-bucket/quarterly/*",
"Condition": { "Bool": { "aws:SecureTransport": "true" } }
}
Say it out loud: allow reading objects under the quarterly/ folder of the reports bucket, but only over HTTPS. That’s a sentence, not a spell.
Build up, don’t cut down
For about a year I did this backwards. I’d start broad and tell myself I’d tighten it later. Later never came, because the ticket closed the second it worked.
Go the other way. Start with nothing. Run the thing. Read the AccessDenied error, which tells you the exact action and resource it wanted, and add only that. The error is basically a to-do list for your policy. Ten minutes of this gets you something tight, instead of something loose you swear you’ll clean up.
Use roles, not users
A user is a permanent identity with long-lived keys. A role is a temporary set of credentials that a service picks up when it needs them and drops afterward.
Your EC2 instances, Lambdas, and containers shouldn’t be holding static keys. They assume a role and get credentials that rotate on their own. If you’re pasting an access key into a config file, stop and go find the role-shaped version of what you’re doing. A leaked user key is good until a human notices and rotates it, which in practice can be months. A leaked role credential is good for an hour.
One habit worth keeping
Before you attach a policy, try to say what it does in one plain sentence. If you can’t, if it comes out as “allow basically everything on this service,” you don’t have a policy yet. Least privilege sounds like extra work. Really it’s just the version of the policy you understand well enough to describe.
Back to Case Files