AWS IAM Explained (2026): Users vs Roles, 9 Policy Types, SCPs and RCPs

Oct 31, 2026 · 14 min · Ajay Kumar

Most AWS IAM questions I get from engineers are one question wearing different clothes: the policy says Allow, so why is the request denied? After fourteen years of AWS accounts, my answer is that nobody's request is judged by a single policy. AWS reads every applicable policy at once, in a fixed order, and any one of nine policy types can say no. This post explains AWS IAM as it works in October 2026: users versus roles, identity versus resource policies, the evaluation logic AWS runs on every call, permission boundaries, SCPs and RCPs, Identity Center, Access Analyzer, and how I would scope an AI agent. If you are staring at an error right now, the sibling post, How to Fix AWS AccessDenied Errors, is the step-by-step version.

What is AWS IAM, and what does it actually decide?

IAM answers two questions for every API call: who is calling, and may this principal perform this action on this resource under these conditions? Per the policy evaluation documentation, AWS authenticates the principal, builds a request context, then evaluates every policy that applies to it.

The request context is the part people skip: principal ARN, account and organization, action, resource ARN, and condition keys such as source IP, VPC endpoint, MFA status, session tags and, more recently, whether the call came through an AWS-managed MCP server. Every request starts as an implicit deny (the root user excepted), and only an explicit Allow that survives every other policy turns it into a yes. So the fix for most denials is not "add more permissions". It is finding which policy in scope said no, and whether the context was what you thought.

IAM users vs IAM roles: which should you use in 2026?

An IAM user has long-term credentials: a console password, access keys, or both. A role has none of its own; a principal assumes it through AWS STS and gets temporary credentials. A role session lasts one hour by default and can be set from 15 minutes up to a 12-hour maximum, per the quotas page, and a chained session is capped at one hour.

AWS's IAM best practices open with two rules: humans federate through an identity provider, workloads use roles. Datadog's 2025 State of Cloud Security shows where the industry is stuck: 79% of organizations federate human console access, yet 39% still use IAM users somewhere, 21% use only IAM users, and 59% of IAM users have an active access key older than a year. Long-lived keys are what ends up leaking, as I covered in Secrets Sprawl 2026.

For humans, the 2026 answer is IAM Identity Center: permission sets assigned per account become roles whose names begin with AWSReservedSSO_. Since 10 August 2026, account access manager can also assign roles that already exist in an account to Identity Center users and groups. For the CLI, aws login (AWS CLI 2.32.0, 19 November 2025) turns a console sign-in into short-lived credentials, removing the last excuse for laptop access keys.

AWS enforced root MFA across all account types on 17 June 2025, and centralized root access lets the management account delete member-account root credentials and run root-only tasks through short sts:AssumeRoot sessions. For workloads the answer is workload identity federation. An IAM user still makes sense for a vendor tool that only accepts static keys, and for little else.

Identity-based vs resource-based policies, and the other seven types

The IAM user guide lists nine policy types. Two grant most access; the rest mostly cap it. An identity-based policy attaches to a user, group or role; a managed one can be 6,144 characters, and a role takes 20 managed policies by default and 25 at most, per the quotas page. A resource-based policy attaches to a bucket, key, queue or role trust policy and names the Principal it trusts.

Within one account, permissions are the union of the two, so either can grant access. Across accounts, both must allow. KMS key policies and role trust policies are exceptions even inside one account: they must explicitly allow access for the principal, per the evaluation logic page.

Policy type Attached to Grants access? Typical job Source
Identity-based Users, groups, roles Yes What this principal may do IAM docs
Resource-based Buckets, keys, queues, trust policies Yes Who may use this resource, other accounts included IAM docs
VPC endpoint policy A VPC endpoint No, limits What may pass through this endpoint IAM docs
Permissions boundary One user or role No, limits Ceiling for roles that teams create themselves IAM docs
SCP Org root, OU or account No, limits Ceiling for principals in member accounts IAM docs
RCP Org root, OU or account No, limits Ceiling for resources in member accounts IAM docs
ACL S3 buckets and objects (legacy) Yes, other accounts only Old S3 grants; disable with Object Ownership IAM docs
AWS RAM share A shared resource Yes Share subnets and more across accounts IAM docs
Session policy One STS session No, limits Narrow a single session below its role IAM docs

How does AWS policy evaluation logic work?

The enforcement logic is a sequence of gates. An explicit Deny in any applicable policy ends it, and nothing overrides that. RCPs and SCPs must each contain an Allow, which they do by default through RCPFullAWSAccess and FullAWSAccess. Then come the resource-based, identity-based, boundary and session policies.

How AWS evaluates one request in one account, top to bottom 1 Explicit Deny in any applicable policy? 2 RCP allows it? (resources in member accounts) 3 SCP allows it? (principals in member accounts) 4 Does a resource-based policy allow it? 5 Does an identity-based policy allow it? 6 Does the permissions boundary allow it? 7 Does the session policy allow it? ALLOW Yes: DENY. Final; no Allow anywhere can override it No Allow in any RCP: DENY (default RCPFullAWSAccess allows) No Allow in any SCP: DENY (default FullAWSAccess allows) Grants the user or role session ARN directly: ALLOW here No: implicit DENY (unless gate 4 already allowed) Boundary set and no Allow: implicit DENY Session policy passed and no Allow: implicit DENY Cross-account: the identity policy AND the resource policy must both allow the request. KMS key policies and role trust policies must explicitly allow the principal even inside one account.
The gates follow the order of AWS's documented enforcement logic for requests within a single account, simplified to one line per policy type. Source: the IAM User Guide pages on policy evaluation logic and access denied troubleshooting.

Gate 4 still surprises experienced engineers. Within one account, a bucket policy that names an IAM user ARN or a role session ARN (arn:aws:sts::111122223333:assumed-role/app/session) allows access despite an implicit deny in the identity policy, boundary or session policy. Name the role ARN (arn:aws:iam::111122223333:role/app) instead and the boundary and session policy still apply. Same role, different ARN, different outcome.

Error messages now tell you which gate failed: "with an explicit deny in a service control policy" or "because no identity-based policy allows the action". Since 21 January 2026 explicit denies within one account or organization also carry the denying policy's ARN, and S3 added it to its 403 responses on 13 August 2026, per Classmethod's write-up. The message names only one policy type even when several deny, per the troubleshooting page, so fixing one denial can reveal the next.

Permission boundaries, SCPs and RCPs: three ceilings aimed at different targets

All three cap permissions; none grants anything. They differ in what they attach to.

A permissions boundary is a managed policy set on one user or role. Effective permissions are the intersection of identity policies and boundary, and resource-based policies are not limited by it. Its real use is delegation: teams create their own roles, each must carry the boundary, so iam:CreateRole is not a path to admin.

A service control policy caps what principals in member accounts can do, including the member account's root user. Per the SCP documentation, SCPs do not affect the management account or service-linked roles. Since 19 September 2025 SCPs accept the full IAM language, including conditions and resource ARNs in Allow statements and NotResource, and the Organizations quotas allow 10,240 characters per SCP and 10 attached per root, OU or account.

A resource control policy caps what anyone, including principals outside your organization, can do to resources in member accounts. RCPs launched on 13 November 2024 for S3, STS, KMS, SQS and Secrets Manager; on 24 October 2026 the supported services list has 62 service prefixes, including ECR, DynamoDB (February 2026), CloudWatch Logs and Cognito (January 2026). RCPs skip the management account, service-linked roles and AWS managed KMS keys, and are capped at 5,120 characters and five per entity.

My mental model: SCP is "what my people can do", RCP is "what anyone can do to my things", boundary is "what the roles my people create can do". Adoption is low: Datadog found 40% of AWS Organizations users run SCPs and 6% run RCPs, and only 0.1% of organizations use an RCP as a data perimeter. An RCP that limits S3 and KMS to aws:PrincipalOrgID, with an exception for AWS service principals, is one of the cheapest controls in AWS. Test it on one OU first, as the RCP page warns.

What changed in AWS IAM in 2025 and 2026?

Date Change Why it matters Source
17 Jun 2025 Root MFA enforced for all account types Member-account root users included AWS
17 Jun 2025 Access Analyzer internal access findings Who inside the org can reach S3, DynamoDB, RDS snapshots AWS
19 Sep 2025 Full IAM language in SCPs Conditions and resource ARNs in Allow AWS
19 Nov 2025 Outbound identity federation AWS roles get signed JWTs for external services AWS
21 Jan 2026 Policy ARN in access denied messages Explicit denies name the policy AWS
6 May 2026 AWS MCP Server GA Agents call AWS through one audited server AWS
10 Aug 2026 IAM account access manager Assign existing roles to Identity Center users AWS
25 Aug 2026 Access troubleshooter preview (CLI 2.36.31) Authorization ID in denials, IAM APIs first IAM docs, CLI changelog

Most of that list is about explaining denials and setting organization-wide ceilings, not about new ways to grant access.

IAM Access Analyzer: what each analyzer finds and costs

IAM Access Analyzer is several tools sharing a name. The pricing page is the clearest map:

  • External access (free): resources shared outside your zone of trust. Enable it per Region.
  • Unused access ($0.20 per IAM role or user per month): unused roles, keys, passwords and permissions. An organization with 2,000 roles and users pays $400 a month.
  • Internal access ($9 per monitored resource per Region per month): which principals in your organization can reach chosen S3 buckets, DynamoDB tables or RDS snapshots. Pick only the most sensitive stores.
  • Custom policy checks ($0.002 per call): check-no-new-access, check-access-not-granted and check-no-public-access, built for CI. A thousand checks a month cost $2.
  • Policy generation (free): drafts a policy from CloudTrail activity.

The custom checks are the underused piece: a pipeline that fails when a Terraform plan grants new access catches the mistake when it is cheapest.

How do you apply least privilege to AI agents on AWS?

Agents are workloads with unpredictable call patterns, so workload rules apply harder (the broader question is in Non-Human Identity in 2026). Give each agent its own role, never a human's session. Datadog found 12.2% of third-party integration roles overprivileged in 2025, up from 10%; agent roles are the next batch of integration roles and will drift the same way unless something prunes them. Narrow each run further with a session policy passed on AssumeRole, and require a source identity so CloudTrail shows which task made each call. Put a permissions boundary on any role the agent can create.

The AWS MCP Server, GA since 6 May 2026, acts with the caller's own credentials, and IAM has two condition keys for it: aws:ViaAWSMCPService (boolean, true for any AWS-managed MCP server) and aws:CalledViaAWSMCP (the specific server, such as aws-mcp.amazonaws.com). Denies written against the older aws-mcp:InvokeMcp actions no longer have any effect. The keys only cover calls through an AWS-managed server; an agent shelling out to the CLI looks like any other client.

IAM Policy Autopilot (release 0.3.0 on 18 August 2026) drafts identity policies from application code; treat its output as a draft for Access Analyzer to validate. For agents that call services outside AWS, outbound identity federation lets a role exchange its credentials for a short-lived signed JWT instead of carrying a stored API key. Disclosure: I run PandaStack, a Firecracker microVM cloud for agent code execution. The rule I would apply to any sandbox, mine included: code inside gets no ambient cloud credentials unless a task needs a specific, scoped role.

# Terraform 1.17 / AWS provider 6.70.0 (released 22 Oct 2026). Account IDs are placeholders.
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "6.70.0" }
  }
}

# Org-wide guardrail: nobody, human or agent, deletes data through an AWS MCP server.
resource "aws_organizations_policy" "no_deletes_via_mcp" {
  name = "deny-destructive-via-mcp"
  type = "SERVICE_CONTROL_POLICY"
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid       = "DenyDestructiveViaMcp"
      Effect    = "Deny"
      Action    = ["s3:DeleteBucket", "s3:DeleteObject", "dynamodb:DeleteTable", "rds:DeleteDBInstance"]
      Resource  = "*"
      Condition = { Bool = { "aws:ViaAWSMCPService" = "true" } }
    }]
  })
}

# Attach to a test OU first; move it up the tree once CloudTrail shows nothing breaking.
resource "aws_organizations_policy_attachment" "no_deletes_via_mcp" {
  policy_id = aws_organizations_policy.no_deletes_via_mcp.id
  target_id = "ou-abcd-11111111"
}

# Ceiling for the agent role, whatever gets attached to it later.
resource "aws_iam_policy" "agent_boundary" {
  name = "agent-boundary"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      { Sid = "Ceiling", Effect = "Allow", Action = ["dynamodb:GetItem", "dynamodb:Query", "s3:GetObject"], Resource = "*" },
      { Sid = "NoIdentityWork", Effect = "Deny", Action = ["iam:*", "organizations:*"], Resource = "*" }
    ]
  })
}

resource "aws_iam_role" "triage_agent" {
  name                 = "triage-agent"
  max_session_duration = 3600 # role chaining caps sessions at 1 hour anyway
  permissions_boundary = aws_iam_policy.agent_boundary.arn
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:aws:iam::111122223333:role/agent-runner" }
      # The runner's own policy also needs sts:AssumeRole and sts:SetSourceIdentity on this role.
      Action    = ["sts:AssumeRole", "sts:SetSourceIdentity"]
      # Every session must say which ticket it is working on; CloudTrail records it.
      Condition = { StringLike = { "sts:SourceIdentity" = "ticket-*" } }
    }]
  })
}

# What the role may do at most for any task; each run narrows it again with a session policy.
resource "aws_iam_role_policy" "triage_agent" {
  name = "read-tickets"
  role = aws_iam_role.triage_agent.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["dynamodb:GetItem", "dynamodb:Query"]
      Resource = "arn:aws:dynamodb:eu-west-1:111122223333:table/tickets"
    }]
  })
}

At runtime the runner calls aws sts assume-role with --source-identity ticket-4812, a --policy scoped to that ticket's table keys, and --duration-seconds 900. Least privilege for agents is ordinary IAM discipline, applied per task instead of per service.

FAQ

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

An IAM user has long-term credentials, a password or access keys, valid until someone deletes them. A role has none; principals assume it and receive temporary credentials that expire after 15 minutes to 12 hours. AWS recommends roles for workloads and federation for humans.

Does an explicit deny always override an allow in AWS?

Yes. AWS checks every applicable policy for a matching Deny first, and one match is final regardless of any Allow elsewhere. The catch is scope: a Deny only counts where its policy type applies, so an SCP Deny never touches the management account or service-linked roles.

What is the difference between an SCP and an RCP?

An SCP limits what principals in your member accounts can do, wherever the resource lives. An RCP limits what any principal, including outside accounts, can do to resources in your member accounts. Neither grants permissions or touches the management account.

Do permission boundaries limit resource-based policies?

No. A permissions boundary caps the identity-based permissions of one user or role. Within one account, a resource-based policy that names the user or role session ARN directly can still allow access, while one that names the role ARN is limited by the boundary.

This quarter, inheriting an AWS organization, I would turn on the unused access analyzer and delete every access key older than a year that it flags, move the remaining human IAM users to Identity Center, enable centralized root access and remove member-account root credentials, attach an org-only RCP for S3 and KMS to one test OU, and add the aws:ViaAWSMCPService deny before the first engineer connects a coding agent to production. None of that is new technology: it is the least-privilege work IAM always needed, for a new class of caller that finds gaps faster.

Related: Workload Identity Federation Explained (2026): OIDC Instead of Cloud Keys, How to Use GitHub Actions OIDC With AWS, GCP and Azure (2026) and Who Is This Agent? Non-Human Identity in 2026, From the RFCs to the Breaches.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related