IAM Mental Model
Principals, actions, resources — how requests get allow or deny.
IAM Mental Model
Every cloud request is a four-tuple: (principal, action, resource, context). IAM's job is to answer one question about that tuple: allow or deny?
If you remember nothing else, remember this: an IAM request is either explicitly allowed by some statement, explicitly denied by some statement (which wins), or implicitly denied because no statement matched. That's the entire algorithm.
Analogy
Think of a nightclub with a paranoid door policy. The bouncer sees four things about each person: who they are, what they want to do ("enter", "enter VIP"), where ("main floor", "rooftop"), and the context (it's 3am, it's raining, they're with a member). They flip through a binder of rules: if one page says "yes, let them in" and another page says "no, banned for life", the ban always wins. If no page says anything at all, the default is "no, step aside". Kind but firm, and utterly rule-bound.
The four things IAM sees
| Part | Example |
|---|---|
| Principal | arn:aws:iam::111:user/alice, role/lambda-exec, service/s3.amazonaws.com |
| Action | s3:GetObject, dynamodb:Query, iam:CreateUser |
| Resource | arn:aws:s3:::bucket/key, arn:aws:dynamodb:us-east-1:111:table/orders |
| Condition | source IP, MFA present, request time, encryption context |
Policies are JSON documents with statements. Each statement is an Allow or Deny scoped to actions and resources, optionally gated by conditions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::app-data", "arn:aws:s3:::app-data/*"]
}
]
}
The evaluation algorithm
- Collect every policy that could apply to the request: identity-based, resource-based, permissions boundaries, SCPs, session policies.
- If any policy has a matching Deny statement — the final answer is DENY. Stop.
- Otherwise, if any policy has a matching Allow — ALLOW.
- Otherwise, implicit DENY.
Explicit Deny always wins. That's why permissions boundaries and service control policies work: they bolt a Deny fence around what a user could otherwise do.
Least privilege in practice
The default starting position is zero permissions. You grant the smallest scope that unblocks the task:
- Scope actions:
s3:GetObject, nots3:*. - Scope resources:
arn:aws:s3:::app-logs/*, not*. - Scope principals: one role per workload, not one shared admin role.
- Add conditions:
aws:SourceIp,aws:MultiFactorAuthPresent,s3:x-amz-server-side-encryption.
The trap is that Allow: * feels productive during development and becomes permanent. Start tight and loosen on a concrete, logged 403.
Identity-based vs resource-based
| Policy type | Attached to | Answers |
|---|---|---|
| Identity-based | A user, group, or role | "What can this principal do?" |
| Resource-based | A bucket, queue, KMS key, Lambda | "Who can touch this resource?" |
| Permissions boundary | An IAM role | "What's the maximum this role can ever do?" |
| SCP (Organizations) | An OU or account | "What can anyone in this account ever do?" |
For cross-account access you usually need both: a resource-based policy on the resource and an identity-based policy on the principal. Either side alone is not enough.
Roles vs users
Prefer roles over long-lived IAM users. A role is a set of permissions any principal can temporarily assume, producing short-lived STS credentials. Users with static access keys are a credential leak waiting to happen.
Workload access patterns:
- EC2 / ECS / Lambda: attach an execution role, SDK auto-assumes it.
- CI/CD: use OIDC federation (GitHub Actions, GitLab) — no secrets in CI config.
- Developers: federate through SSO / Identity Center, assume roles per account.
The traps
- Wildcards in resources that look scoped.
arn:aws:s3:::bucket/logs/*does not match the bucket itself (arn:aws:s3:::bucket).ListBucketfails silently. - Explicit Deny on the wrong scope.
Deny s3:*on a resource you don't own blocks nothing. - Stale policies. IAM is eventually consistent; a change can take seconds to propagate. Re-run after a retry window before debugging further.
- Trust policies vs permissions policies. A role has both. The trust policy says "who can assume me"; the permissions policy says "what I can then do". Both must match.
The one-line rule
Start with nothing. Grant the specific action on the specific resource, logged. Walk away when the test passes.
Tools in the wild
6 tools- serviceAWS IAM Access Analyzerfree tier
Surfaces externally-shared resources + suggests least-privilege policy reductions.
- serviceAWS Policy Simulatorfree tier
Try `Allow`/`Deny` decisions against any IAM principal without touching production.
- libraryCedarfree tier
AWS's open-source policy language (the engine behind AVP); embeds in any app.
- libraryOpen Policy Agent (OPA)free tier
General-purpose policy engine — Rego rules over JSON; runs anywhere.
- cliProwlerfree tier
Open-source AWS/GCP/Azure security audit tool; finds over-broad IAM, public buckets.
- cliiamlivefree tier
Records the actual IAM actions a CLI/SDK call makes; perfect for least-privilege design.