IAM: users, roles and least privilege in practice
IAM — Identity and Access Management — is how AWS decides who can do what. It is the most important security system in your account, the thing most often misconfigured, and the source of both "access denied when it should work" and "that intern could delete production" problems. Getting IAM right — especially least privilege — is a defining skill of an AWS engineer. This lesson is IAM's model and how to apply least privilege without grinding your team to a halt.
The IAM model: identities, policies, permissions
IAM has a few core concepts, and they fit together simply once named:
- Principals — the who: an IAM user (a person or app with credentials), an IAM role (a set of permissions something can assume temporarily — the preferred mechanism, below), or an AWS service acting on your behalf.
- Policies — JSON documents that define permissions: which actions (e.g.
s3:GetObject) are allowed or denied on which resources, under which conditions. - Permissions — the result of evaluating the policies attached to a principal.
You grant access by attaching policies to identities (or roles). A policy is JSON with a clear shape:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}
]
}
Read it: allow the actions s3:GetObject and s3:PutObject on any object in my-app-bucket. Every AWS
permission is expressed this way — an effect (Allow/Deny), actions, resources, and optional conditions. Learning
to read and write these policies is core IAM fluency.
Least privilege: grant only what is needed
The single most important IAM principle is least privilege: give each identity only the permissions it
actually needs, and nothing more. The opposite — attaching AdministratorAccess to everyone because it is
easy — means any compromised credential or careless action can do anything, which is how small mistakes
become catastrophes.
Least privilege in practice:
- Start from nothing and add what is needed. Grant the specific actions a role requires for its job, not
broad wildcards. A deployment role needs to push images and update a service — not
*:*. - Scope to specific resources.
Resource: "arn:aws:s3:::my-app-bucket/*"(this one bucket), notResource: "*"(every bucket in the account). Narrowing the resource is as important as narrowing the action. - Prefer AWS-managed policies for common cases, but review them. AWS provides managed policies (e.g.
AmazonS3ReadOnlyAccess); they are convenient, but some are broader than you need — check before attaching. - An explicit
Denyalways wins. In policy evaluation, an explicitDenyoverrides anyAllow, which is how SCPs and deny statements enforce hard boundaries.
The honest tension — and the reason least privilege is a skill — is that too tight blocks your team ("access denied" on something they legitimately need), and too loose is insecure. You resolve it by granting narrowly, then widening deliberately when a real need appears, using the tools below to find the right scope — rather than starting wide "to be safe", which is never safe.
Roles over users: the pattern that matters
Here is the most important practical guidance in this lesson: prefer roles to users with long-lived credentials.
- An IAM user can have a long-lived access key (an ID and secret). Those keys do not expire, so if one leaks — committed to Git, logged, phished — it is valid until someone notices and revokes it. Long-lived keys are the most common serious AWS security incident.
- An IAM role is assumed to get temporary credentials that expire automatically (minutes to hours). Nothing long-lived to leak. A person assumes a role (via single sign-on — the next lesson), and an AWS service (an EC2 instance, an EKS pod, a Lambda) is given a role so it gets temporary credentials automatically — no keys stored anywhere.
So the pattern is: humans get access by assuming roles through SSO; services get roles attached to them; and you avoid long-lived access keys almost entirely. For example, an application on EC2 does not store AWS keys — you attach an instance role, and the AWS SDK automatically uses its temporary credentials. This eliminates the biggest class of AWS credential leaks. The access-patterns lesson goes deeper; the takeaway here is that roles-with-temporary-credentials, not users-with-keys, is how modern AWS access is done.
Tools to get the scope right
Least privilege is easier with AWS's own tooling, which you should know exists:
- IAM Access Analyzer — can generate a least-privilege policy from the actions an identity actually used (from CloudTrail history), so you tighten a broad policy down to what is really needed rather than guessing.
- The IAM policy simulator — test whether a policy would allow or deny a specific action before you rely on it.
- CloudTrail — records every API call, so you can see what an identity did and diagnose "why was this denied?" or "what does this role actually use?".
- Permissions boundaries — an advanced control that caps what a role's policies can grant (like an SCP, but per-identity), useful when you let teams create their own roles within limits.
The workflow that keeps you sane: grant a reasonable starting scope, watch what is actually used (Access Analyzer / CloudTrail), and tighten to that — rather than either starting with admin or blocking everyone.
Check your work
IAM model: principals (users, roles, services) do things; policies (JSON: Effect Allow/Deny, Actions, Resources, Conditions) define permissions; you attach policies to identities. Every permission is a policy statement — read/write these fluently.
Least privilege (the key principle): grant only what is needed — start from nothing and add; scope to
specific resources (my-app-bucket/*, not *) as well as specific actions; review AWS-managed policies (some
too broad); an explicit Deny always wins. Too tight blocks the team, too loose is insecure — grant narrowly
then widen deliberately, never start wide.
Roles over long-lived keys (the pattern that matters): IAM user access keys don't expire (leak = valid till revoked — the top AWS incident); roles are assumed for temporary, expiring credentials. Humans assume roles via SSO; services get roles attached (instance role, pod role, Lambda role) — no stored keys. Avoid long-lived access keys almost entirely.
Tooling: IAM Access Analyzer (generate least-privilege from actual usage), policy simulator (test before relying), CloudTrail (what happened / why denied), permissions boundaries (cap what a role can grant). Grant, observe, tighten.
Practice
- Read the sample S3 policy and state exactly what it allows and on what.
- Rewrite an over-broad policy (
Action: "*",Resource: "*") to least privilege for a role that only reads one bucket. - Explain the difference between an IAM user with an access key and a role with temporary credentials, and why the role is safer.
- Explain how a service (say, an app on EC2) should get AWS permissions without storing any keys.
- Explain the tension in least privilege (too tight vs too loose) and the grant-observe-tighten workflow.
- Describe how you would use CloudTrail or Access Analyzer to right-size a role's permissions.
Official documentation
- AWS IAM — User guide — Users, roles, policies and how evaluation works.
- AWS IAM — Security best practices — Least privilege, roles over keys, and more.
- AWS IAM — Policies and permissions — The JSON policy language in detail.
Next: human and machine access without long-lived keys.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship