---
title: "04 — IAM: Who Can Do What, Written Down as JSON"
uid: iam-security
tags: ["aws", "security", "permissions", "roadmap:aws", "roles", "iam", "policies"]
excerpt: "IAM is one four-part statement repeated as JSON: who (principal), can do what (action), to which resource, under what conditions. Read every IAM screen as that sentence and the maze collapses."
date: 2026-08-13T03:28:31+0000
source: https://www.aveshina.my.id/en/blog/iam-security
---

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.

```figure
<svg viewBox="0 0 640 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two paths to AWS access. Left: a Human uses a long-lived password/access key to log in as an IAM User. Right: an EC2 instance assumes an IAM Role via an instance profile and receives temporary STS credentials. The role path shows no long-lived secret on the instance.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- Left: user -->
    <text x="160" y="22" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Long-lived identity</text>
    <rect x="60" y="40" width="80" height="40" rx="8" fill="#e0e7ff" stroke="#6366f1"/><text x="100" y="64" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">Human</text>
    <path d="M140,60 L180,60" stroke="#64748b" stroke-width="1.5" marker-end="url(#iarrow2)"/>
    <rect x="180" y="40" width="120" height="40" rx="8" fill="#fef9c3" stroke="#ca8a04"/><text x="240" y="58" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">IAM User</text><text x="240" y="72" font-size="9" fill="#422006" text-anchor="middle">access key (long-lived)</text>
    <rect x="180" y="110" width="120" height="80" rx="8" fill="#dcfce7" stroke="#16a34a"/><text x="240" y="135" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">Permissions</text><text x="240" y="158" font-size="9" fill="#052e16" text-anchor="middle">policy attached</text><text x="240" y="172" font-size="9" fill="#052e16" text-anchor="middle">to the user</text>
    <line x1="240" y1="80" x2="240" y2="110" stroke="#64748b" stroke-width="1.5" marker-end="url(#iarrow2)"/>

    <!-- Right: role -->
    <text x="480" y="22" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Temporary identity</text>
    <rect x="380" y="40" width="80" height="40" rx="8" fill="#e0e7ff" stroke="#6366f1"/><text x="420" y="64" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">EC2</text>
    <path d="M460,60 L500,60" stroke="#64748b" stroke-width="1.5" marker-end="url(#iarrow2)"/>
    <rect x="500" y="40" width="120" height="40" rx="8" fill="#fce7f3" stroke="#db2777"/><text x="560" y="58" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">Assume Role</text><text x="560" y="72" font-size="9" fill="#500724" text-anchor="middle">STS temp creds</text>
    <rect x="500" y="110" width="120" height="80" rx="8" fill="#dcfce7" stroke="#16a34a"/><text x="560" y="135" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">Permissions</text><text x="560" y="158" font-size="9" fill="#052e16" text-anchor="middle">policy attached</text><text x="560" y="172" font-size="9" fill="#052e16" text-anchor="middle">to the role</text>
    <line x1="560" y1="80" x2="560" y2="110" stroke="#64748b" stroke-width="1.5" marker-end="url(#iarrow2)"/>
    <defs><marker id="iarrow2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#64748b"/></marker></defs>
  </g>
</svg>
```

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](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](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](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](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html)

```quiz
Q: In an IAM policy statement, the four parts are…
- Principal, Action, Resource, Condition
- User, Group, Role, Policy
correct: 0
explain: Every statement is who (principal), can do what (action), to which resource, under what condition. User/Group/Role are identity types, not the policy grammar.

Q: What's the difference between an identity-based and a resource-based policy?
- Identity-based attaches to a user/group/role; resource-based attaches to the resource itself and includes a Principal
- Identity-based is JSON; resource-based is YAML
correct: 0
explain: Identity-based grants an identity permissions. Resource-based is attached to the resource (S3 bucket, KMS key) and must state which principals it trusts.

Q: Why are IAM roles preferred over long-lived IAM user access keys for applications?
- Roles issue temporary STS credentials, so there's nothing long-lived to leak
- Roles are cheaper than users
correct: 0
explain: Assuming a role returns short-lived credentials. Nothing is checked into code, and the role's permissions can be changed centrally. Long-lived keys are a leading cause of cloud incidents.

Q: The default IAM decision for any request with no matching Allow statement is…
- Allow
- Deny
correct: 1
explain: Default deny. Access is granted only by an explicit Allow that isn't overridden by an explicit Deny.

Q: An EC2 instance needs to read from an S3 bucket. The idiomatic way to grant that is…
- Generate an access key, store it in user data
- Attach a role (via an instance profile) whose policy allows s3:GetObject on the bucket
correct: 1
explain: The instance assumes the role and receives temporary credentials from STS — no long-lived key, no secret in user data, and the policy can be tightened centrally.
```
