awsiamcloud-securitydevopsaccess-controlaws-sts·Updated ·12 min read

What Is AWS IAM and How Does It Work?

AWS IAM controls who can make a request to AWS (authentication) and whether it is permitted (authorization). Permissions are JSON policies attached to users, groups, roles or resources. On every API call AWS evaluates them: an explicit Deny always wins, otherwise one Allow is enough, and anything not allowed is implicitly denied.

The short version

AWS Identity and Access Management (IAM) is the service that stands in front of every AWS API call and decides two things: who is making this request (authentication) and are they allowed to do it (authorization). Nothing in AWS happens without passing through it.

That is more literal than it sounds. When you click "Create bucket" in the AWS console, when you type aws s3 ls in a terminal, when your application calls dynamodb.putItem() — all three become the same thing: a signed HTTPS request to an AWS API endpoint. IAM inspects that request, works out which identity signed it, gathers every policy that applies, and returns either the result or AccessDenied. The console is just a web app making those same API calls on your behalf.

Two properties are worth internalising early. IAM is a global service — a user, group, role or policy is not tied to a Region, so a role you create works the same whether your EC2 instance is in Mumbai or Ireland. And IAM itself costs nothing; you pay only for whatever the identities go on to use.

The rest of this article walks the two halves — the identities that make requests, and the policies that describe what is permitted — and then shows exactly how AWS puts them together on each call.

The four identity types

An identity that can make a request is called a principal. There are four kinds you will meet.

The account root user is created when the AWS account is opened. It signs in with the account's email address and password, and it has complete, unrestricted access to everything, including billing. Critically, you cannot restrict it with an identity-based policy — there is no policy you can write that takes power away from root. So the guidance is defensive rather than restrictive: turn on MFA (multi-factor authentication — a second factor such as a phone app on top of the password), do not create access keys for it at all, and sign in as root only for the handful of tasks that genuinely require it, such as closing the account or changing the AWS Support plan.

An IAM user is a long-lived identity with its own credentials: a console password, and/or access keys — a pair consisting of an access key ID and a secret access key that the SDKs use to sign requests. A user can hold a maximum of two access keys at once, and that limit exists for a reason: it is exactly enough to rotate keys with no downtime. Create the second key, deploy it everywhere, confirm the old one has stopped being used, then delete the old one.

An IAM group is a container for users. You attach a policy to the group once instead of to twenty users individually. Two quirks catch people out: groups cannot be nested (a group holds users, never other groups), and a group is not a principal — you cannot name a group in a policy's Principal element, because a group is a management convenience, not something that makes requests.

An IAM role is the interesting one. A role has permissions policies but no long-term credentials. Instead, anything permitted to assume the role is handed a set of temporary security credentials that expire. This is the identity you give to an EC2 instance, a Lambda function, or another AWS account.

Some default quotas worth remembering, since certification exams ask and real projects hit them:

Thing Default limit
IAM users per account 5,000
IAM groups per account 300
IAM roles per account 1,000
Customer managed policies per account 1,500
Groups one user can belong to 10
Managed policies per user, group or role 10 (adjustable to 20)
Access keys per user 2

Reading a policy document

Permissions are written as JSON. Here is a complete, minimal identity-based policy:

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

Element by element. Version is the policy language version, not a version number for this document — always use "2012-10-17", because the older "2008-10-17" does not support policy variables and AWS advises against it. Statement holds one statement or an array of them. Sid is an optional label for humans. Effect must be exactly Allow or Deny. Action lists API operations, namespaced by service. Resource lists what those actions may touch, as ARNs.

Notice that the two S3 ARNs are different. arn:aws:s3:::example-bucket is the bucket itself, which is what ListBucket acts on; arn:aws:s3:::example-bucket/* is every object inside it, which is what GetObject acts on. Getting this pair wrong is the single most common reason a beginner's S3 policy "does nothing".

An ARN (Amazon Resource Name) follows the shape arn:partition:service:region:account-id:resource-id, with variants that use resource-type/resource-id or resource-type:resource-id. For standard AWS Regions the partition is aws. Note that the region and account fields are simply left empty in the S3 ARNs above.

Two elements are missing from the example. Principal names who the policy applies to, and it only appears in resource-based policies and role trust policies — in an identity-based policy the principal is implicitly whoever the policy is attached to. Condition adds fine-grained tests using condition keys. (Attribute-based access control, ABAC, is built entirely out of tag condition keys — a separate topic.)

Identity-based vs resource-based

An identity-based policy attaches to a user, group or role. A resource-based policy attaches to the resource — an S3 bucket policy, a KMS key policy, an SQS queue policy, a Lambda resource policy — and always names a Principal. Both can grant the same access; the difference is which side of the relationship you edit.

Policies also come in three flavours: AWS managed (created and maintained by AWS), customer managed (yours, versioned — up to 5 stored versions, after which you must delete one before creating another), and inline (embedded in exactly one identity and deleted along with it).

Warning

Policy documents have hard character limits: 6,144 characters for a managed policy, 2,048 for an inline user policy, 5,120 for an inline group policy, 10,240 for an inline role policy. Whitespace does not count. Teams that write one enormous least-privilege policy per service hit the managed-policy ceiling and then have to split it across attachments — of which you only get 10 by default.

How AWS evaluates a request

This is the mechanism the question is really asking about. On every single API call, AWS:

  1. Authenticates the principal from the request signature.
  2. Builds a request context — the action, the resource, and every condition key it can derive (source IP, whether MFA was used, time, tags, and so on).
  3. Collects every policy that could apply to that principal and resource.
  4. Evaluates them, in this order.

The evaluation logic reduces to three rules:

Rule 1 — the default is deny. Every request starts implicitly denied. If no policy says Allow, the answer is no. The one exception is the root user, who has full access by definition.

Rule 2 — an explicit Deny beats everything. If any applicable policy contains a matching "Effect": "Deny", the request is denied, full stop. No number of Allow statements anywhere else can rescue it. This is the final, overriding step, and it is the sentence to remember.

Rule 3 — otherwise, one Allow is enough. A single matching Allow in any applicable identity-based or resource-based policy flips the implicit deny.

Within one account, AWS walks these layers in order: Organizations SCPs, resource control policies, resource-based policies, permissions boundaries, session policies, and identity-based policies. A deny at any layer ends it. You do not need to know the org-level layers to use IAM day to day — just know that the explicit-deny rule holds at every one of them, so a Deny in a service control policy quietly overrides a perfectly correct IAM policy underneath.

A worked example

Say Priya is an IAM user in the developers group. Three things apply to her:

  • The group has an AWS managed policy attached that allows s3:* on *.
  • Her user has an inline policy denying s3:DeleteObject on arn:aws:s3:::prod-invoices/*.
  • The prod-invoices bucket has a bucket policy allowing her account full access.

She calls s3:GetObject on prod-invoices/jan.pdf. Group policy allows, nothing denies, bucket policy also allows → allowed.

She calls s3:DeleteObject on the same object. The group's managed policy allows it. The bucket policy allows it. Her inline policy explicitly denies it → denied, and the two allows are irrelevant. Rule 2 is not a tiebreaker; it is a veto.

She calls dynamodb:Scan on a table. Nothing in any of the three policies mentions DynamoDB → nothing allows it → denied by implicit deny. Note the difference from the previous case: this one you fix by adding permissions, that one you fix by removing a Deny. When something breaks, that distinction tells you where to look.

Cross-account access needs allows on both sides: the trusting account must permit it via a resource-based policy or a role trust policy, and the calling principal must have an identity-based policy in its own account permitting the same call. One side alone is not enough.

When something is denied and you cannot see why, reach for the IAM Policy Simulator (tests a hypothetical action against real policies without making a real call) and IAM Access Analyzer, which validates policies against IAM grammar and best practices, finds resources shared with external entities, and can generate a policy from recorded CloudTrail activity.

[!CAUTION] Reacting to AccessDenied by widening the policy until the error stops. You end up with "Action": "*" on "Resource": "*" and no idea which permission was actually missing. Work out the one action that was missing — the Policy Simulator will tell you — and add only that.

Roles and temporary credentials in practice

A role carries two policies with different jobs. The trust policy is a resource-based policy that answers "who may assume this role" — it is where Principal appears. The permissions policies answer "what may the resulting session do".

A trust policy for an EC2 instance looks like this:

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

Assuming a role calls AWS STS (Security Token Service). AssumeRole returns three values: an access key ID, a secret access key, and a session token. All three must be sent with each request; the session token is what marks the credentials as temporary. DurationSeconds defaults to 3600 seconds (1 hour) and accepts 900 seconds (15 minutes) up to the role's configured maximum session duration, which itself can be set from 1 to 12 hours and defaults to 1 hour. For IAM users, GetSessionToken is different: 43,200 seconds (12 hours) by default, adjustable from 900 to 129,600 seconds (36 hours).

On EC2, you attach an instance profile — the container that carries a role onto an instance. The SDK on the instance fetches credentials from the Instance Metadata Service at 169.254.169.254, and AWS rotates them automatically before they expire. Use IMDSv2, which is session-oriented: PUT to http://169.254.169.254/latest/api/token to get a token, then send it in the X-aws-ec2-metadata-token header on subsequent GETs. The token TTL is set with X-aws-ec2-metadata-token-ttl-seconds, from 1 to 21,600 seconds (6 hours).

TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

The first command opens a metadata session; the second lists the role name attached to the instance. Applications on the instance retrieve the role's temporary credentials from this same metadata service, which is what the SDK does for you, so you rarely write this by hand.

On Lambda, the execution role is assumed by Lambda when it invokes your function, and the credentials arrive as the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN environment variables. Default SDK credential resolution picks them up with no configuration.

For third parties assuming a role in your account, add the ExternalId condition to the trust policy — AWS recommends it to protect against the confused deputy problem. For CI and Kubernetes, AssumeRoleWithWebIdentity handles OIDC federation (GitHub Actions, EKS service accounts) and AssumeRoleWithSAML handles corporate SAML 2.0 providers — both give you roles instead of stored keys, which is the same reasoning behind keeping configuration out of the image itself.

Interview tip

"What is the difference between an IAM user and an IAM role?" is worth being able to answer in one breath. The answer is not "one is for people" — it is that a user holds long-term credentials, while a role holds none and issues short-lived STS credentials to whoever is trusted to assume it.

Guardrails that actually matter

The security best practices worth applying on day one:

  • Enable MFA on the root user and on every privileged human identity.
  • Never use root for daily work.
  • Prefer roles and temporary credentials over IAM users with access keys — AWS's own guidance is to use temporary credentials rather than long-term access keys.
  • If keys must exist, rotate them (that is what the two-key limit is for) and delete unused ones.
  • Grant least privilege: start from an AWS managed policy or an Access Analyzer policy generated from CloudTrail activity, then tighten.
  • Review last-accessed data on policies and strip permissions nobody has used — the same discipline as pruning a service's dependency list, which is worth doing whether you are setting up a JDK or an AWS account.
  • Test with the Policy Simulator before shipping.

Three topics deliberately not covered here, each a separate rabbit hole: permissions boundaries (a managed policy that caps the maximum permissions an identity can have — effective permissions become the intersection of its policies and the boundary), service control policies in AWS Organizations (account-wide ceilings), and IAM Identity Center. Get the deny-allow-deny evaluation flow and the role-plus-STS pattern solid first; the rest are refinements on top of the same mechanism, in much the same way that understanding what triggers a Kafka rebalance matters more than memorising every coordinator config.

Frequently asked questions

Is IAM free, and does it cost anything to create thousands of users or roles?
IAM is offered at no additional charge. You are billed only for the other AWS services that your identities go on to use. Creating users, groups, roles and policies costs nothing, though default quotas cap you at 5,000 users, 300 groups and 1,000 roles per account.
Why does my S3 policy allow GetObject but ListBucket still fails?
Because they act on two different ARNs. Object-level actions like s3:GetObject need arn:aws:s3:::bucket-name/*, while bucket-level actions like s3:ListBucket need arn:aws:s3:::bucket-name with no trailing slash. A policy that lists only one of the two ARNs will silently deny half the operations you expected to work.
If I attach an AWS managed policy that allows an action, can a Deny elsewhere still block it?
Yes, always. An explicit Deny in any applicable policy — an inline policy on the user, a resource-based policy, a session policy or an Organizations SCP — overrides every Allow. It is the final and overriding step of policy evaluation, not a tiebreaker, so no additional Allow will ever override it. A permissions boundary works differently: it does not usually contain an explicit Deny, it simply caps what can be allowed, so your effective permissions are the intersection of your identity-based policies and the boundary.
How long do STS temporary credentials last, and what happens when they expire?
AssumeRole credentials default to 3600 seconds (1 hour) and can range from 900 seconds up to the role's maximum session duration, which is configurable from 1 to 12 hours. GetSessionToken for an IAM user defaults to 12 hours with a 900-second to 36-hour range. On EC2, you do not have to handle expiry yourself — AWS rotates the credentials in the instance metadata service before they expire, and the SDKs refetch them.
Can I put a group in the Principal element of a bucket policy?
No. An IAM group is a management container for attaching policies to multiple users at once, not a principal that makes requests, so it cannot appear in a Principal element. If you need a shared identity in a resource-based policy, name the individual users, the account, or create a role and name the role.
What is the difference between an inline policy and a customer managed policy?
An inline policy is embedded directly in one user, group or role and is deleted when that identity is deleted; it cannot be reused elsewhere. A customer managed policy is a standalone object you can attach to many identities and it keeps up to 5 stored versions, so you can roll back. Inline policies also have different character limits — 2,048 for a user, 5,120 for a group, 10,240 for a role, versus 6,144 for a managed policy.

References

  1. What is IAM? - AWS Identity and Access Management User GuideAWS Documentation
  2. Policy evaluation logic - AWS IAM User GuideAWS Documentation
  3. IAM JSON policy elements referenceAWS Documentation
  4. IAM identities (users, user groups, and roles)AWS Documentation
  5. IAM roles - AWS Identity and Access ManagementAWS Documentation
  6. Policies and permissions in IAMAWS Documentation
  7. IAM and AWS STS quotas, name requirements, and character limitsAWS Documentation
  8. AssumeRole - AWS Security Token Service API ReferenceAWS Documentation
  9. GetSessionToken - AWS Security Token Service API ReferenceAWS Documentation
  10. Security best practices in IAMAWS Documentation
  11. Using an IAM role to grant permissions to applications running on Amazon EC2 instancesAWS Documentation
  12. Use IMDSv2 - Amazon EC2 User GuideAWS Documentation
  13. AWS account root user - AWS IAM User GuideAWS Documentation
  14. Amazon Resource Names (ARNs) - AWS General ReferenceAWS Documentation
  15. Using AWS Identity and Access Management Access AnalyzerAWS Documentation
  16. Lambda execution role - AWS Lambda Developer GuideAWS Documentation