AWS IAM Explained: Roles vs Policies vs Users
AWS IAM (Identity and Access Management) is the service that decides who can do what to which resource in your AWS account — and it is the single most misunderstood corner of the cloud. Get it right and it is an invisible safety net; get it wrong and you either can't deploy or you hand an attacker the keys to production. This guide is AWS IAM explained for developers, with a clear line drawn between its three core building blocks — users, roles, and policies — and a practical rule for when to reach for each. It sits in the Devgains cloud cluster alongside the Terraform pillar, because in a real account you will provision every one of these objects as code.
Quick answer: users vs roles vs policies
The three IAM primitives answer three different questions:
- A user is a permanent identity for a specific human or long-lived integration. It answers "who is making this request?"
- A role is a temporary identity that any trusted principal can assume to get short-lived credentials. It answers "what can this workload act as right now?"
- A policy is a JSON document listing permissions that you attach to a user or role. It answers "what is this identity allowed to do?"
The one-line mental model: users and roles are identities; policies are the permissions you staple onto them. Users are who you are; roles are who you can temporarily become; policies are what either one is allowed to do.
Why AWS IAM matters
IAM is the blast-radius control for your entire cloud footprint. Every API call — from the console, the CLI, an EC2 instance, or a Lambda function — is authenticated to a principal and then authorized against policy before AWS lets it through. Two things make this worth learning properly:
- Over-permissioning is the default failure mode. It is far easier to attach
AdministratorAccessand move on than to scope permissions to exactly what a service needs. Every over-broad policy is a bigger target, and IAM misconfiguration is a leading root cause of cloud breaches. - Long-lived credentials leak. Access keys committed to Git, baked into container images, or left in environment variables are a recurring incident. Roles exist precisely so that workloads never hold static keys — a direct application of the same discipline in keeping secrets out of environment variables.
IAM done well is the practical face of Zero Trust: grant the least privilege necessary, prefer short-lived credentials, and verify every request.
The three primitives in detail
Users
An IAM user is a named identity with long-term credentials: a console password and/or a set of access keys (an access key ID and secret access key). Users are meant for real humans or, in legacy setups, for an application that runs outside AWS and genuinely cannot assume a role.
The modern guidance is blunt: create as few users as possible. For human access, federate through AWS IAM Identity Center (formerly AWS SSO) or your existing identity provider so people log in with short-lived sessions instead of permanent keys. Reserve raw IAM users for the rare integration that has no other option.
Roles
An IAM role is an identity with no permanent credentials. Instead, a trusted principal assumes the role and receives temporary security credentials (via AWS STS) that expire automatically — typically in an hour. Roles are how you grant permissions to:
- AWS services — an EC2 instance, Lambda function, or ECS task assumes a role to call other services, with zero static keys involved.
- Federated users — people signing in through SSO or a corporate IdP.
- Cross-account access — an identity in account A assumes a role in account B.
A role has two policy attachments that do different jobs:
- A trust policy (the assume-role policy) — says who is allowed to assume the role.
- One or more permissions policies — say what the role can do once assumed.
Policies
A policy is a JSON document made of statements. Each statement has an Effect
(Allow or Deny), one or more Actions (like s3:GetObject), one or more Resources (ARNs),
and optional Conditions. Policies come in two main flavors:
- Managed policies — standalone, reusable, versioned documents. AWS-managed ones (like
AmazonS3ReadOnlyAccess) are maintained by AWS; customer-managed ones are yours. - Inline policies — embedded directly in a single user or role, with no reuse. Handy for a one-off, but harder to audit at scale.
Architecture: how IAM evaluates a request
When a principal makes a request, IAM runs a deterministic evaluation. Simplified, the logic is:
- Default deny. Every request starts denied.
- Explicit deny wins. If any applicable policy has an explicit
Denyfor the action, the request is denied — full stop. NoAllowcan override it. - Explicit allow required. With no explicit deny, IAM looks for at least one
Allowacross all applicable policies (identity policies, resource policies, and any permission boundaries or Service Control Policies). If none allows it, the default deny stands.
The mental shortcut: an explicit Deny always beats an Allow, and no Allow means denied.
This is why you rarely need Deny statements for everyday permissions — leaving an action out is
already a deny. Reserve explicit Deny for guardrails you never want overridden.
Step-by-step: give a Lambda function least-privilege S3 access
The most common real task is letting a workload touch exactly one resource. Here is the pattern for a Lambda function that should read from a single bucket and nothing else.
1. Write a permissions policy scoped to one bucket. Note the two ARNs — one for the bucket, one for the objects inside it:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadReportsBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::acme-reports",
"arn:aws:s3:::acme-reports/*"
]
}
]
}s3:ListBucket acts on the bucket ARN; s3:GetObject acts on the object ARN (/*). Splitting them
is the single most common IAM mistake — see below.
2. Write a trust policy so only Lambda can assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}3. Create the role and attach the policy with the AWS CLI. The role is born with the trust policy; the permissions policy is attached separately:
# Create the role with its trust policy
aws iam create-role \
--role-name reports-reader \
--assume-role-policy-document file://trust-policy.json
# Create a customer-managed permissions policy
aws iam create-policy \
--policy-name reports-read-only \
--policy-document file://s3-read-policy.json
# Attach it to the role
aws iam attach-role-policy \
--role-name reports-reader \
--policy-arn arn:aws:iam::123456789012:policy/reports-read-onlyAssign reports-reader to your Lambda function and it receives fresh, auto-rotating credentials on
every invocation — no access keys anywhere.
4. In production, provision this as code, not by hand. The same three objects in Terraform, which slots straight into the plan/apply workflow:
resource "aws_iam_role" "reports_reader" {
name = "reports-reader"
assume_role_policy = data.aws_iam_policy_document.trust.json
}
resource "aws_iam_role_policy" "read_s3" {
name = "reports-read-only"
role = aws_iam_role.reports_reader.id
policy = data.aws_iam_policy_document.s3_read.json
}Now the role, its trust relationship, and its permissions live in Git, are reviewed in a pull request, and are tracked in Terraform state like any other resource.
Comparison: users vs roles
| Aspect | IAM User | IAM Role |
|---|---|---|
| Credentials | Long-lived (password, access keys) | Temporary (STS, auto-expiring) |
| Who uses it | Specific humans / legacy apps | Services, federated users, cross-account |
| Key rotation | Manual, easy to forget | Automatic on every assume |
| Attached to | One identity | Anyone the trust policy permits |
| Best for | Rare "no other option" cases | Almost everything in modern AWS |
| Leak blast radius | High — keys work until revoked | Low — credentials expire in ~1 hour |
The takeaway from this table is a rule of thumb: default to roles; use users only when you truly cannot assume a role.
Best practices
- Grant least privilege. Start from zero and add specific actions and resource ARNs. Never
reach for
"Action": "*"or"Resource": "*"in production policies. - Prefer roles and short-lived credentials over long-lived access keys everywhere possible.
- Use permission boundaries to cap the maximum permissions a role can ever have, even if someone attaches a broader policy later.
- Turn on IAM Access Analyzer to surface resources shared externally and to help right-size policies from real CloudTrail usage.
- Enforce MFA on every human identity, and require it via
Conditionfor sensitive actions. - Provision IAM as code so every grant is reviewed, versioned, and auditable.
Common mistakes
- Attaching
AdministratorAccess"for now." It is never temporary. Scope permissions from the start. - Forgetting the bucket-vs-object ARN split.
s3:ListBucketneeds the bucket ARN;s3:GetObjectneeds the/*object ARN. Miss one and you get bafflingAccessDeniederrors. - Baking access keys into code or images. Use a role instead — this is the same class of leak as secrets in environment variables.
- Confusing the trust policy with the permissions policy. The trust policy controls who can assume; permissions policies control what they can do. Both must be right.
- Relying on
Denyfor everyday scoping. Absence ofAllowis already a deny; overusing explicitDenymakes policies hard to reason about.
Key takeaways
- Users and roles are identities; policies are permissions. Keep those roles straight and IAM stops being confusing.
- Default to roles. Temporary, auto-expiring credentials shrink your blast radius far more than any key-rotation policy.
- Evaluation is deny-by-default, an explicit
Denyalways wins, and noAllowmeans denied. - Least privilege plus IAM-as-code is the combination that keeps a growing account safe and auditable.
FAQ
What is the difference between an IAM user and an IAM role? A user is a permanent identity with long-lived credentials, meant for a specific human or legacy integration. A role is a temporary identity with no permanent credentials — a trusted principal assumes it and receives short-lived credentials that expire automatically. Prefer roles for almost everything; use users only when a role genuinely isn't possible.
What is an IAM policy?
An IAM policy is a JSON document listing permissions as statements, each with an Effect
(Allow/Deny), Actions, Resources, and optional Conditions. You attach policies to users or
roles to grant them permissions; a policy by itself does nothing until attached.
When should I use a role instead of a user? Use a role whenever an AWS service (EC2, Lambda, ECS), a federated human, or another AWS account needs access. Roles avoid static keys and give short-lived credentials. Reserve users for the rare external integration that cannot assume a role.
How does AWS IAM decide whether to allow a request?
Every request is denied by default. IAM then checks all applicable policies: any explicit Deny
immediately denies the request, and otherwise at least one explicit Allow is required. If nothing
allows the action, the default deny stands.
What is the difference between a trust policy and a permissions policy? A trust policy (attached to a role) defines who is allowed to assume the role. A permissions policy defines what the identity can do once it is that user or role. A role needs both: a trust policy to be assumable and permissions policies to be useful.
Conclusion
AWS IAM stops being intimidating the moment you separate its three ideas: users and roles are identities, policies are the permissions you attach to them, and roles exist so workloads never have to hold long-lived keys. Default to roles, scope every policy to the exact actions and resources it needs, and provision all of it as code so each grant is reviewed and auditable. That is least privilege in practice — the cloud expression of Zero Trust. From here, continue with the Terraform pillar to provision IAM safely, keep your state secure and locked, and browse the full cloud category and related security guides.
References
- AWS IAM User Guide — the authoritative overview of identities, policies, and evaluation.
- Policy evaluation logic — exactly how allow/deny decisions are made.
- IAM security best practices — least privilege, roles over users, and MFA.
- IAM roles — trust policies, assuming roles, and temporary credentials.
- AWS STS: temporary credentials — how short-lived credentials are issued.

