AWS IAM

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

How AWS IAM decides who can do what. Users, groups, roles and policies, the evaluation order that produces AccessDenied, and why humans should not have IAM users.

IAM is the permission system that decides who can do what in an AWS account. Every other service defers to it, which makes it the one AWS service you cannot avoid understanding.

It is also the one people get wrong most expensively, because the failure modes are asymmetric: an over-permissive policy works perfectly until it is abused, while an under-permissive one fails loudly and gets fixed.

IAM: Identity & Access Model

IAM User
IAM Role
member of / assumes
IAM Group / Trust Policy
has attached
IAM Policy (JSON)
Effect · Action · Resource
grants / denies
AWS Service (S3, EC2, RDS…)

Deny always overrides Allow. Explicit deny wins in every evaluation

Four building blocks

ConceptWhat it isCredentials
UserA long-lived identity, usually a person or legacy automationPermanent password and access keys
GroupA container for users that carries policiesNone (groups cannot be assumed)
RoleAn identity assumed temporarily by anyone or anythingTemporary, expiring credentials
PolicyA JSON document describing allowed or denied actionsN/A

The distinction that matters most is user versus role. A user carries permanent credentials that exist until someone deletes them. A role issues temporary credentials that expire on their own, typically within an hour.

That difference is why the modern guidance is blunt: do not create IAM users for people. Use IAM Identity Center, or federate from your existing identity provider, so humans sign in with corporate credentials and receive a temporary role session. There are no access keys to leak, and offboarding happens in one place.

Keep IAM users only for the cases that genuinely need a long-lived credential and cannot use a role, and treat each one as a liability to be reviewed.

Anatomy of a policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadPaymentsArtifacts",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::acme-artifacts",
        "arn:aws:s3:::acme-artifacts/payments/*"
      ],
      "Condition": {
        "StringEquals": { "aws:PrincipalTag/team": "payments" }
      }
    }
  ]
}

Two details cause most S3 policy failures. s3:ListBucket acts on the bucket ARN while s3:GetObject acts on the object ARN. They are different resources and both are usually needed. And "Version": "2012-10-17" is a policy language version, not a date you should change; omitting it silently disables condition variables.

Condition is where least privilege becomes practical. Rather than enumerating every resource, you can require a matching tag, a source VPC endpoint, MFA, or a specific request region.

How a request is actually evaluated

This is the part worth memorising, because it explains every AccessDenied you will ever see.

  1. Deny by default. No permission at all unless something grants it.
  2. Explicit Deny anywhere wins. Immediately, regardless of any allow.
  3. Service control policies (in an AWS Organization) must permit the action. An SCP never grants anything; it only sets a ceiling.
  4. Permissions boundaries, if attached, must also permit it.
  5. An Allow must exist in an identity-based or resource-based policy.

The practical consequence: an AdministratorAccess policy does not mean you can do everything. An SCP or a permissions boundary above it silently caps what that allow is worth, and the resulting denial names neither.

# Which principal am I actually? First question, always.
aws sts get-caller-identity
{
    "UserId": "AROAX...:alice",
    "Account": "111122223333",
    "Arn": "arn:aws:sts::111122223333:assumed-role/PlatformEngineer/alice"
}
# Would this call be allowed, and which policy decides?
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/PlatformEngineer \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::acme-artifacts/payments/build.zip

simulate-principal-policy is underused and answers the question directly instead of by trial and error.

Roles have two policies, not one

Every role carries two documents, and confusing them is the most common IAM stumbling block.

The trust policy says who may assume the role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

The permissions policy says what the role can do once assumed.

An AccessDenied on sts:AssumeRole is a trust policy problem. An AccessDenied on s3:GetObject after successfully assuming the role is a permissions policy problem. The error tells you which, if you read which action was denied.

aws sts assume-role \
  --role-arn arn:aws:iam::111122223333:role/DeployRole \
  --role-session-name ci-build-482

The --role-session-name appears in CloudTrail, so putting a build number or username there turns an anonymous role session into an auditable one.

How workloads should get permissions

Never embed access keys in an application. Every AWS compute service can assume a role automatically:

Running onMechanism
EC2Instance profile
LambdaExecution role
ECSTask role: not the execution role, which is for pulling images and writing logs
EKSPod Identity, or IRSA
GitHub Actions / GitLab CIOIDC federation to a role

That last row deserves emphasis. OIDC removes long-lived AWS keys from CI entirely. The pipeline presents a signed token, AWS exchanges it for temporary credentials, and the trust policy restricts which repository and branch may do so. If your CI still holds an AWS_SECRET_ACCESS_KEY, this is the highest-value IAM change available to you.

Getting to least privilege without guessing

Starting from AdministratorAccess "for now" never gets narrowed. Start from nothing and add what fails, but use the data AWS already has rather than guessing:

# What has this role actually used, and when?
aws iam generate-service-last-accessed-details --arn arn:aws:iam::111122223333:role/DeployRole
# Which credentials exist, and which are stale?
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d

IAM Access Analyzer goes further: it reviews CloudTrail history for a role and generates a policy containing only the actions actually used. That is a far better starting point than a hand-written guess, and it is the fastest route from a permissive policy to a defensible one.

Access Advisor in the console shows services a principal has not touched in months, usually the easiest permissions to remove safely.

The account-level things to get right once

  • Lock the root user away. Hardware MFA, no access keys, used only for the handful of tasks that genuinely require it.
  • Enable MFA everywhere, and require it in policy conditions for sensitive actions rather than trusting people to enable it.
  • Turn on CloudTrail in all regions before you need it. IAM without an audit trail is unauditable by definition.
  • Use permissions boundaries when delegating IAM to teams, so they can create roles without being able to escalate beyond a ceiling you set.
  • Prefer customer-managed policies to inline ones. Inline policies are invisible in a policy listing and cannot be reused or reviewed centrally.

Go deeper in this section

Sub-topicWhat you will learn
IAM Users and GroupsManaging identities and group-based permissions
IAM Roles and PoliciesTrust policies, assume-role, and policy types
IAM Best PracticesLeast privilege in practice, MFA and auditing

Quick reference

TaskCommand
Who am Iaws sts get-caller-identity
Assume a roleaws sts assume-role --role-arn <arn> --role-session-name <name>
Test a permissionaws iam simulate-principal-policy --policy-source-arn <arn> --action-names <action>
List rolesaws iam list-roles
Policies on a roleaws iam list-attached-role-policies --role-name <name>
Read a policyaws iam get-policy-version --policy-arn <arn> --version-id <v>
Credential reportaws iam generate-credential-report
Unused permissionsaws iam generate-service-last-accessed-details --arn <arn>

Key takeaways

  • Roles issue expiring credentials; users hold permanent ones. Prefer roles almost everywhere.
  • Do not create IAM users for people. Use IAM Identity Center or federation.
  • Explicit Deny always wins, and an SCP or permissions boundary can cap even AdministratorAccess.
  • A role has two policies: trust decides who may assume it, permissions decide what it can do.
  • s3:ListBucket targets the bucket ARN, s3:GetObject the object ARN. Both are usually needed.
  • Use OIDC for CI so no long-lived AWS keys exist in your pipeline.
  • Let Access Analyzer generate least-privilege policies from real CloudTrail usage instead of guessing.

Check your understanding

Exercise

Admin that is not admin

A user with the AdministratorAccess policy attached gets AccessDenied when creating an S3 bucket in eu-west-1. The policy is definitely attached. What is the most likely cause?

Exercise

Two policies, two failures

A CI pipeline fails with AccessDenied on sts:AssumeRole when trying to use a deployment role. Which document should you check?


Frequently Asked Questions

What is the difference between an IAM user and an IAM role?

A user is a long-lived identity with permanent credentials. A console password, and often access keys that remain valid until someone deletes them. A role is an identity that anyone or anything permitted by its trust policy can assume temporarily, receiving credentials that expire automatically, typically within an hour. Roles are safer because there is no lasting secret to leak, and they are what EC2 instances, Lambda functions, ECS tasks and CI pipelines should use. Current AWS guidance is to avoid creating IAM users for people at all, using IAM Identity Center or federation instead.

Why am I getting AccessDenied when my policy allows the action?

Work through the evaluation order. An explicit Deny anywhere, in an identity policy, a resource policy, or a service control policy, beats every allow. A service control policy in an AWS Organization sets a ceiling that even AdministratorAccess cannot exceed, and region restrictions are a common form of this. A permissions boundary on the principal does the same. Run aws sts get-caller-identity to confirm which principal you actually are, then aws iam simulate-principal-policy to see whether the call would be permitted and what decides it.

What is the difference between a trust policy and a permissions policy?

Both attach to a role and answer different questions. The trust policy names the principals allowed to assume the role: a service such as ec2.amazonaws.com, another account, or a federated identity provider. The permissions policy defines what the role may do once assumed. If you are denied on sts:AssumeRole, the trust policy is refusing you; if you assume the role successfully and are then denied on an action, the permissions policy is the problem. Reading which action was denied tells you which document to open.

How do I give an EC2 instance or Lambda function access to S3?

Attach a role, never access keys. For EC2 that means an instance profile; for Lambda an execution role; for ECS a task role; for EKS, Pod Identity or IRSA. The SDK on that compute picks the credentials up automatically from the instance metadata service or the environment, with no configuration in your code. Embedding access keys in an application or an AMI is the single most common way AWS credentials leak, because they end up in images, repositories and logs.

How do I remove AWS access keys from my CI pipeline?

Use OIDC federation. Register your CI provider, GitHub Actions, GitLab, and most others support this, as an OIDC identity provider in your AWS account, then create a role whose trust policy accepts tokens from it with conditions restricting which repository and branch may assume it. The pipeline exchanges a short-lived signed token for temporary AWS credentials at run time. Nothing long-lived is stored anywhere, there is nothing to rotate, and a leaked pipeline log contains no reusable secret.

How do I actually achieve least privilege without breaking things?

Use the data AWS already collects rather than guessing. IAM Access Analyzer can generate a policy from a role's real CloudTrail activity, giving you a starting point containing only actions genuinely used. Access Advisor shows which services a principal has not touched in months, which are the safest permissions to remove first. Apply changes in a non-production account and watch CloudTrail for denials before promoting. Starting broad with the intention of narrowing later almost never results in narrowing.

Continue Learning

Explore Related Topics

When it breaks

Related Resources