AWS IAM Cheat Sheet: Policies, Roles, and the JSON That Decides Access
A practical AWS IAM cheat sheet covering users, groups, roles, policy JSON (Effect/Action/Resource/Condition), least privilege, and a read-only S3 example.
AWS IAM Cheat Sheet: Policies, Roles, and the JSON That Decides Access
IAM is the part of AWS that quietly decides whether every single API call succeeds or returns AccessDenied. Most of the confusion around it comes from four building blocks that look similar but behave very differently: users, groups, roles, and policies. Once those click, the policy JSON stops feeling like a riddle and starts reading like a sentence: this principal is allowed to do this action on this resource, under these conditions.
This cheat sheet walks through the concepts, the exact shape of a policy document, the difference between roles and users, and one fully worked least-privilege example you can copy.
The Four Building Blocks
Everything in IAM is one of these:
- User — a long-lived identity for a human or a legacy script. Carries a password or access keys. You authenticate as the user.
- Group — a bucket of users that share attached policies. Groups have no credentials; they are an attachment convenience. Put "Developers" in a group, attach one policy, done.
- Role — an identity with permissions but no permanent credentials. Something assumes it and receives temporary keys that expire (often in an hour). EC2 instances, Lambda functions, CI pipelines, and cross-account access all use roles.
- Policy — a JSON document listing what is allowed or denied. Policies attach to users, groups, and roles. They are the rules; the others are the things the rules apply to.
A quick mental model: users and roles are who, policies are what they can do, and groups are how you avoid copy-pasting policies onto twenty people.
Anatomy of a Policy Document
Every identity-based policy is a JSON object with a Version and one or more Statement blocks. Each statement has up to four fields that matter most:
- Effect —
AlloworDeny. - Action — the API operations, like
s3:GetObjectorec2:StartInstances. Wildcards are legal (s3:*) and usually a smell. - Resource — the ARN(s) the actions apply to.
- Condition — optional guardrails (source IP, MFA present, a tag must match, a specific service).
Here is a minimal statement:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::reports-prod/*",
"Condition": {
"Bool": { "aws:SecureTransport": "true" }
}
}
]
}
Read it left to right: allow the s3:GetObject action on every object inside the reports-prod bucket, but only when the request arrives over HTTPS. The /* at the end of the ARN is doing critical work — more on that in the worked example.
The evaluation rule that trips people up: an explicit Deny always wins, no matter how many Allow statements exist. And if nothing matches, the default is an implicit deny. You are never granting access by removing a deny; you grant it with an Allow and then narrow it.
Roles vs Users: When to Reach for Which
This is the distinction worth tattooing somewhere. A user is a fixed identity with credentials that live until you rotate them. A role is assumed on demand and hands back short-lived credentials.
Consider the same task — letting an application read from a bucket — done two ways:
USER approach (avoid for workloads):
IAM user "app-prod" with access keys baked into a config file.
Keys never expire on their own. Leak one, and it is valid until noticed.
ROLE approach (preferred):
EC2 instance assumes role "app-read-role".
AWS rotates the temporary keys automatically every few hours.
Nothing sensitive sits on disk.
The role wins because there is no static secret to leak. Reach for users only for the handful of cases that genuinely need them (a human's console login, or a tool that cannot use the instance metadata service), and prefer IAM Identity Center over raw users where you can. For anything running on AWS compute, the answer is almost always a role.
Roles also carry a second policy you do not see on users: the trust policy, which says who is allowed to assume me. That is where service principals, cross-account IDs, ExternalId, and OIDC CI patterns live.
Worked Example: Least-Privilege Read-Only on One Bucket
Say a reporting dashboard needs to list and download objects from one bucket, quarterly-reports, and nothing else. Least privilege means we grant exactly that — no s3:*, no other buckets, no write.
The trap is that S3 has two ARN levels. Bucket-level actions like s3:ListBucket use the bucket ARN; object-level actions like s3:GetObject use the bucket ARN plus /*. Confuse them and the policy looks right but matches nothing. Here is the correct version:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListTheBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::quarterly-reports"
},
{
"Sid": "ReadTheObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::quarterly-reports/*"
}
]
}
Two statements, two ARN shapes, zero wildcards on actions. The dashboard can enumerate the bucket and pull files, but it cannot delete, upload, or touch any other bucket. If someone later asks "why can it read but the list fails," check whether the ListBucket statement accidentally got the /* suffix — that mismatch is the single most common S3 IAM bug I see.
When I am reviewing a teammate's policy, I paste it straight into the AWS IAM cheat sheet inspector before I read it line by line. It flags wildcard actions, wildcard resources, iam:PassRole exposure, and exactly this kind of missing-/* mistake in a second, which means I spend my attention on intent instead of spotting typos. It runs entirely in the browser, so I can do it on a locked-down laptop without the policy ever leaving the tab.
Conditions and the PassRole Landmine
Conditions are where good policies get tight. A few you will reach for constantly:
aws:SourceIp— restrict to an office or VPN range.aws:MultiFactorAuthPresent— require MFA for sensitive actions.aws:RequestTag/aws:ResourceTag— enforce tag-based access.iam:PassedToService— pin which service a role can be passed to.
That last one guards the most dangerous action in IAM: iam:PassRole. PassRole lets a user hand a role to a compute service like EC2, Lambda, or ECS. If a deployer can pass an admin role to a service they control, they can run code as that role and effectively own the account. Never grant iam:PassRole on "Resource": "*". Scope it to specific role ARNs and add a PassedToService condition so the role can only be handed to the service you intended.
Quick Reference
| Need | Use | Key field | |------|-----|-----------| | Human console access | User (or Identity Center) | password + MFA | | App on EC2/Lambda reads AWS | Role | trust policy | | Cross-account access | Role + ExternalId | Principal | | Read one S3 bucket | s3:ListBucket + s3:GetObject | object ARN needs /* | | Block something absolutely | Explicit Deny | wins over all Allows | | Limit who passes a role | iam:PassRole scoped | iam:PassedToService |
A few rules of thumb to close on. Start every policy from zero and add only what fails. Treat * in either Action or Resource as a question, not a default. Remember that arn:aws:iam::account:root in a Principal delegates to the whole account, not just the root user. And once your policy looks done, validate it for real with aws iam simulate-principal-policy or IAM Access Analyzer — a static check confirms the shape, but only AWS confirms the answer. If you are stitching IAM into a wider deploy workflow, the AWS CLI cheat sheet covers the commands that go alongside these policies.
Made by Toolora · Updated 2026-06-13