AWS IAM: Policy Evaluation, Trust Boundaries, and Why Your Permissions Are Being Denied

3 min readCloud Infrastructure

IAM denials are rarely mysterious once you understand the evaluation order. This post covers how AWS evaluates a request through identity-based policies, resource-based policies, SCPs, and permission boundaries — and where most production mistakes happen.

awsiamsecurity

Why "I gave it the right permissions" still produces Access Denied

IAM evaluation is not a single policy check. AWS evaluates a request through multiple policy layers in a defined order. An explicit deny in any layer overrides every allow elsewhere. A missing permission in any required layer is also a denial.

The full evaluation order for an IAM request:

  1. Service Control Policies (SCPs) — org-level ceiling. If the SCP does not allow the action, it is denied regardless of identity policies.
  2. Resource-based policies — if present, these can grant access to principals in other accounts.
  3. IAM permission boundaries — developer-defined ceiling on what an identity-based policy can grant.
  4. Session policies — applied when assuming a role via sts:AssumeRole with inline policy.
  5. Identity-based policies — the policies attached to the user, group, or role.

The default is implicit deny: if no policy explicitly allows the action, it is denied. An explicit Deny statement overrides any allow from any other policy.

IAM evaluation logic

ConceptAWS IAM

AWS evaluates each request against all applicable policies. The decision is: allow if at least one policy grants the action AND no policy explicitly denies it AND all required layers permit it.

Prerequisites

  • AWS services and resources
  • JSON structure basics
  • ARN format

Key Points

  • Explicit Deny beats everything. A Deny in any policy layer cannot be overridden by an Allow elsewhere.
  • Implicit deny is the default. No allow statement means access is denied, silently.
  • Cross-account access requires permission on both sides: the identity policy (caller) AND the resource policy (target).
  • SCPs set the ceiling. An SCP that doesn't allow an action cannot be overridden by any identity policy, even AdministratorAccess.
  • Permission boundaries limit the maximum effective permissions but do not grant permissions on their own.

The two policy types on a role and why they are different

Every IAM role has two separate policy mechanisms:

Trust policy — controls who can assume the role. It is a resource-based policy attached to the role itself. Without a trust policy entry for the caller, sts:AssumeRole fails regardless of any permission policies.

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

Permission policy — controls what the role can do once assumed. Multiple policies can be attached.

A common mistake: attaching permissions without updating the trust policy. The role has the right capabilities, but nothing is allowed to assume it. Or the trust policy lists the right principal but the permission policy is missing the action.

💡Debugging Access Denied: start with IAM Policy Simulator

AWS IAM Policy Simulator evaluates all applicable policies for a given principal + action + resource combination and shows you exactly which policy produced the allow or deny. Use it before spending time reading policy JSON manually.

For access denied in CloudTrail logs, look for errorCode: "AccessDenied" with errorMessage. The message often names the specific policy that denied the request: "...with an explicit deny in a resource-based policy" vs "...because no identity-based policy allows".

These are different problems. The first requires removing a Deny statement. The second requires adding an Allow statement.

Identity-based vs resource-based policies

Identity-based policies are attached to IAM principals: users, groups, roles. They travel with the principal.

Resource-based policies are attached directly to resources: S3 buckets, SQS queues, KMS keys, Lambda functions, and others. They specify which principals can access the resource.

For same-account access, either type alone is sufficient. If an IAM role has S3 permission in its identity policy, it can access the bucket without the bucket needing a bucket policy.

For cross-account access, both sides must explicitly allow the access:

Account A (caller):         Account B (resource):
Role policy: Allow          Bucket policy: Allow
  s3:GetObject                s3:GetObject from Account A
  arn:B:s3:::bucket           Principal: arn:aws:iam::AccountA:root

If either side is missing, the cross-account access is denied. This is the most common source of cross-account permission confusion: engineers add the permission to the role but forget the resource policy, or vice versa.

Service Control Policies: the limit above your account

SCPs apply to AWS accounts and OUs inside an AWS Organization. They define the maximum permissions available to all principals in that account — including the root user.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Action": ["ec2:*"],
    "Resource": "*",
    "Condition": {
      "StringNotEquals": {
        "aws:RequestedRegion": ["us-east-1", "us-west-2"]
      }
    }
  }]
}

This SCP denies all EC2 actions outside the allowed regions, for every principal in every account under the OU. An engineer with AdministratorAccess in the account still cannot create an EC2 instance in eu-west-1.

SCPs do not grant permissions. A permissive SCP (allow all) simply means the SCP layer does not restrict anything — the identity-based policy still determines what is allowed.

SCP gotcha: the management account is exempt

SCPs do not apply to the AWS Organizations management account. Actions taken from the management account always bypass SCP restrictions. This is a security consideration: workloads should not run in the management account, precisely because SCPs — your primary org-level control — cannot constrain it.

Permission boundaries: capping delegated administration

Permission boundaries let you safely delegate IAM creation to a team without letting them escalate beyond a defined ceiling. A developer can create roles for their application, but only with permissions the boundary permits.

// Permission boundary: limits what any role created by this team can do
{
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:*", "dynamodb:*", "logs:*"],
    "Resource": "*"
  }]
}

A role with AdministratorAccess identity policy but a restrictive permission boundary can only perform the actions the boundary allows. The effective permission is the intersection of the identity policy and the boundary.

Permission boundaries do not replace the identity policy — they cap it. A boundary allowing S3 does not grant S3 access to a role whose identity policy does not include it.

Common IAM permission designs

Two patterns for controlling what a service can access. Both work; the tradeoffs are in maintainability and blast radius.

Broad role + resource conditions
  • Role has wide action permissions (s3:*) restricted by ARN conditions
  • Simpler to read for engineers new to the account
  • Condition drift is common — conditions get loosened under deadline pressure
  • Works well when resources are well-named and ARN patterns are stable
Narrow role + explicit resource ARNs
  • Role lists only the specific actions and resource ARNs it needs
  • More verbose and requires updates when resources change
  • Blast radius of a compromised role is bounded by the ARNs listed
  • Easier to audit and review in security assessments
Verdict

Prefer narrow roles with explicit ARNs for production services. The maintenance overhead is worth the reduced blast radius. Use resource conditions for cases where ARNs cannot be predicted at policy-write time (e.g., prefix-based access to S3 bucket paths).

A Lambda function has an IAM execution role with full S3 permissions (s3:*). The function tries to write to an S3 bucket in a different AWS account and receives Access Denied. The S3 bucket policy allows the Lambda's role ARN. What is the most likely cause?

medium

Same-region, cross-account access. No SCPs are in place. The bucket policy includes the correct Lambda role ARN with s3:PutObject Allow.

  • AThe Lambda execution role needs s3:PutObject explicitly — s3:* is not recognized cross-account
    Incorrect.s3:* includes s3:PutObject. Wildcard actions work cross-account the same as same-account.
  • BThe Lambda execution role's identity policy must explicitly allow the cross-account bucket ARN
    Correct!Cross-account access requires permission on both sides. The bucket policy allows the caller — that satisfies Account B. But Account A's identity policy must also allow s3:PutObject on the specific cross-account bucket ARN. s3:* on Resource: * covers it, but if the identity policy only allows s3:* on same-account resources, the cross-account ARN is excluded.
  • CCross-account S3 access always requires an IAM role in Account B to be assumed first
    Incorrect.Not always. Resource-based policies on S3 can grant direct cross-account access without role assumption, provided both sides have the permission.
  • DThe bucket policy must use the Lambda function ARN, not the role ARN
    Incorrect.IAM evaluates requests against the role ARN. The Lambda function ARN is not the IAM principal for permission evaluation.

Hint:Cross-account S3 access requires permission from both the caller's identity policy and the resource's bucket policy.