IAM (Identity and Access Management) is the AWS system that answers one question: "who can do what." Want to read a file from storage, launch a virtual machine, send a message to a queue — every time, AWS checks with IAM: are you even allowed to do that?

It sounds like boring bureaucracy, but this is where most cloud accidents actually happen. Systems usually get broken not through a sophisticated hack, but because someone was granted too many permissions or an access key accidentally ended up in a public repository on GitHub. That's why spending half an hour understanding IAM pays off more than untangling an incident later. Let's take it step by step.

Users vs Roles

This is the first and most important distinction that beginners confuse.

An IAM user is a permanent account. It has a name and a pair of long-lived access keys (an access key and a secret key) — something like a login and password, but for programs. These keys don't change on their own: you create them once and use them until you revoke them.

An IAM role is not an account, but a set of permissions that you can temporarily "put on." An analogy: a user is your permanent passport, while a role is a visitor badge at the reception desk, issued for a couple of hours and meaningless on its own without you. The process of "putting on a role" is called assume role, and in return you get not permanent keys, but temporary credentials that expire on their own after an hour or two.

The key rule to remember right away: for programs, servers, and automation, use roles, not users with keys.

Here's why:

  • User keys live forever, so sooner or later they leak — into code, into logs, into terminal command history, into a screenshot. And a leaked key keeps working until someone manually notices.
  • Role credentials are issued for minutes to hours and refresh automatically. Even if such a set leaks, an hour later it's already useless.

In practice it looks like this: your application runs on a virtual machine (EC2), in a container (ECS), or in Kubernetes (EKS). You attach a role to it — and the application gets its permissions through that role. There is not a single key in the code or the configs. When a beginner asks "how does my service reach the S3 storage?" — the correct answer is almost always "through a role," not "put the keys in environment variables."

How Policies Work

Permissions themselves are described by a policy — a document in JSON format that lists what is and isn't allowed. Here's the simplest policy — "allow reading objects from a specific S3 bucket (storage container)":

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

Let's break it down field by field:

  • Version — the version of the policy language. The date 2012-10-17 doesn't mean the document is outdated; it's simply a fixed identifier for the current grammar, and it's always written exactly this way.
  • EffectAllow or Deny.
  • Action — what exactly: s3:GetObject means "download an object." Each service has its own list of actions.
  • Resource — over what: here it's the ARN (Amazon Resource Name, the global address of a resource) of a specific bucket.

Policies come in two kinds, and this pair matters:

  • Identity-based — attached to a user or a role. It reads as "this role is allowed to read from this bucket."
  • Resource-based — attached to the resource itself. It reads the other way around: "this bucket allows access to such-and-such role or account." Such policies are needed, for example, to open access from another account, or for services like S3 and SQS queues.

How AWS makes its decision can be reduced to three rules:

  1. By default everything is denied (this is called implicit deny). If no policy has explicitly allowed an action — it's denied.
  2. Access is granted only if there is an explicit Allow. Within a single account, an Allow in any one of the policies is enough — identity- or resource-based, it doesn't matter which. But for access between accounts, both sides must allow it: the identity policy on the side of whoever is making the request, and the resource policy on the side of the resource.
  3. An explicit Deny beats any Allow. If a deny for that action is set anywhere at all — that's it, no access, and no permissions can override it.

Additionally, each rule can have a Condition block that narrows access even further: "only if the request came from such-and-such network," "only with confirmation via MFA (a second factor)," "only with encryption."

Least Privilege — the Minimum of Permissions

This is the main principle of all IAM security: grant exactly as many permissions as the task needs, and not one line more.

The most common beginner mistake is to write "Action": "*" and "Resource": "*" — that is, "everything is allowed on everything." People do it "to save trouble" — and that's exactly what later turns into an incident: one leaked role like this hands an attacker the whole account.

In practice, least privilege is an iterative process, not a one-time setup:

  1. Start narrow — grant the minimum that is definitely needed.
  2. Run it, see what's missing (AWS shows denials in the logs, and dedicated access analyzers suggest which permissions are actually used).
  3. Add what's missing. Repeat.

Yes, this takes longer than granting everything at once. But "narrow and by actual need" is the boundary that separates normal operation from the headline about yet another data breach.

STS and Temporary Credentials

Behind all temporary credentials stands a dedicated service — STS (Security Token Service). When someone "assumes a role," under the hood the AssumeRole operation is called, and STS returns a short-lived set: an access key, a secret key, and a session token. By default they live for one hour, and the maximum can be configured up to 12 hours.

The manual command looks like this:

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/my-app-role \
  --role-session-name demo-session

The most useful thing for a beginner is to realize that almost everything is built on this mechanism:

  • A virtual machine's role (instance role) quietly calls STS for you — the SDK gets and refreshes the credentials itself, and you don't have to write anything.
  • In Kubernetes (EKS), the same thing is done by mechanisms like IRSA or the newer Pod Identity — they issue a pod the temporary credentials of a role.
  • Access between accounts is assuming a role in someone else's account.
  • Logging in through a corporate account (federation) is also the issuance of temporary credentials.

Once "credentials are temporary, and they're obtained through assume" clicks in your head — most questions about "where does a service even get access without keys in the code" answer themselves.

Cross-Account Access

In AWS, accounts are the primary isolation boundary: by default there's no way to reach from one account into another. When such access is nonetheless needed (for example, a service in account A must read data in account B), it's done through a role, not through shared keys.

The scheme is as follows:

  1. In the account that owns the resource (B), a role is created.
  2. A trust policy is attached to the role — it says "this role is allowed to be assumed by account A."
  3. To the same role an identity policy with the needed permissions is attached (for example, reading the bucket).
  4. The service in account A calls AssumeRole for this role, gets temporary credentials, and works with them.

Important: no shared keys are passed between accounts. Trust is configured declaratively through the trust policy, and actual access always goes through temporary credentials.

Where This Applies

IAM is the "who can do what" layer on top of the network layer of "who can even reach whom." Together they provide full protection: a minimal network plus minimal permissions. That's why it's worth learning them as a pair.

Where you'll run into IAM right away:

  • Any of your services in the cloud (virtual machines, serverless functions, containers) gets access to other services through a role.
  • Code delivery pipelines (CI/CD) also work through roles, not through hardcoded keys — this is covered in pipeline principles.
  • In Kubernetes, pod access to cloud services is configured through roles — see deploy and configuration.

Typical beginner mistakes to avoid from day one:

  • Putting long-lived user keys in code or environment variables instead of using a role.
  • Granting "*" in Action or Resource "to make it work," and then forgetting to narrow it down.
  • Being surprised that access is denied even though there's an Allow — almost always the culprit is an explicit Deny somewhere higher up, or a forgotten implicit deny (nothing allowed it anywhere).
  • Storing keys in shared access for communication between accounts instead of setting up a role with a trust policy.

What to learn next: exactly how secrets are stored and data is encrypted — in security and observability; how roles are described in infrastructure-as-code so you don't have to click around the console by hand — in IaC fundamentals and Terraform; and a general view of reliable architecture — in Well-Architected.