04 — IAM: Who Can Do What, Written Down as JSON
Every IAM console looked like a maze of users, roles, and policies — until I realized it was one sentence repeated. The model that held: IAM is a set of permissions, written as JSON, that I attach to an identity — a user, a group, or a role — to answer one question: who can do what to which resource under what conditions [1]. Everything in AWS security is a variation on that. Once I read every IAM screen as the same four-part statement, the service stopped being a maze.
The core: principals, actions, resources, conditions
IAM's mental grammar has four nouns [2]:
- Principal — who is making the request. A user, a role, an AWS service, even another AWS account.
- Action — what they want to do. Always written as service:verb — s3:GetObject, ec2:RunInstances, iam:PassRole.
- Resource — which thing the action targets, given as an ARN (Amazon Resource Name). arn:aws:s3:::my-bucket/* means every object in my-bucket.
- Condition — when it's allowed. Optional. "Only from this IP range," "only with MFA," "only outside business hours."
A policy is just a JSON document bundling statements of those four. The decision engine evaluates the request, finds matching statements, and the answer is allow only if an explicit allow exists and no explicit deny overrides it. Default is deny.
Policies: the JSON shape
The two policy families I had to keep apart [2]:
- Identity-based — attached to a user, group, or role. "This identity may do X." The vast majority of policies are this kind.
- Resource-based — attached directly to a resource (an S3 bucket, a KMS key, an SNS topic). "These principals may do X to me." Resource-based policies include a Principal element because the resource needs to say who.
A minimal identity-based policy that lets someone read from one bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}The Version is a date string (not the version of my policy) and is required. Effect is Allow or Deny. That's the whole grammar. The hard part is not the syntax — it's writing the smallest allow that covers the real need, because every broad permission is a future incident.
There's also a managed-versus-inline split: managed policies are standalone, reusable, and versioned (AWS maintains hundreds of AWS-managed ones); inline policies live one-to-one on a single identity and disappear with it. I default to managed policies for anything I'd want to repeat.
Users and groups: for humans
An IAM user is a long-lived identity with credentials (password for console, access keys for CLI/SDK) [3]. A group is a collection of users that shares a set of policies [3]. The pattern that scales: put people into groups named by job function (developers, billing-admins, dbas), attach policies to the group, and let users inherit. Adding or removing a person becomes a one-line group membership change instead of a policy rewrite.
The trap to avoid: long-lived access keys checked into code or config. Once a key leaks, it's valid until rotated. The safer answer for anything programmatic is a role.
Roles: for services and short-lived trust
A role is an identity with permissions, but no permanent credentials [4]. Instead, a trusted entity assumes the role and receives temporary credentials from STS (Security Token Service). Two canonical uses:
- Service roles — "this EC2 instance may assume this role" (delivered through an instance profile), so code running on the instance gets AWS credentials without me baking keys into user data. Same pattern for Lambda, ECS tasks, and every other service.
- Cross-account or federated access — "users from account B may assume this role in account A," which is how organizations share access without distributing keys.
The payoff: nothing long-lived to leak. Temporary credentials expire in minutes to hours, and the role's permissions can be tightened centrally. This is why IAM roles are the backbone of any sane AWS security posture, and why the worst smell in an account is a pile of IAM users with long-lived access keys attached to policies called AdministratorAccess.
The rule I keep: if it has credentials I might leak, it should be a role. Users are for humans logging into a console; roles are for everything else.
How I use this
IAM is the one AWS service I treat as never-finished. The habit is a recurring audit: turn on IAM Credentials Report, find every long-lived access key, and replace each with a role assumed by the workload that actually needs it; prune every * action or * resource that crept in during a hurry; enforce MFA on every human user; and put the root user behind a hardware MFA and then never use it. The mental test for any new permission is the four-part grammar — who, what, which, when — stated out loud before the policy is written. If I can't state it plainly, the policy isn't ready.
References
[1] Amazon Web Services, "IAM introduction," IAM User Guide, 2024. [Online]. Available: https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html
[2] Amazon Web Services, "Identity-based policies and resource-based policies," IAM User Guide, 2024. [Online]. Available: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_identity-vs-resource.html
[3] Amazon Web Services, "IAM users and groups," IAM User Guide, 2024. [Online]. Available: https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction_identity-management.html
[4] Amazon Web Services, "IAM roles," IAM User Guide, 2024. [Online]. Available: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html
Knowledge check · Question 1 of 5
In an IAM policy statement, the four parts are…
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!