IAM Permission Boundaries: Delegating Safely Without Losing Control

Bits Lovers
Written by Bits Lovers on
IAM Permission Boundaries: Delegating Safely Without Losing Control

The problem: your application team needs to create IAM roles for their Lambda functions and ECS tasks. You can give them iam:CreateRole and related permissions, but then they can create a role with AdministratorAccess and assume it — effectively granting themselves full account access without any approval. You can refuse to delegate IAM and handle every role creation yourself, but that creates a bottleneck. Permission boundaries solve this by letting you delegate IAM administration within enforced limits.

A permission boundary is an IAM managed policy that sets the ceiling on what permissions a user or role can ever have. It doesn’t grant anything by itself — it restricts. A developer with full S3 permissions and a permission boundary of ec2:DescribeInstances can only describe EC2 instances, not touch S3 at all. The effective permission set is always the intersection of what the identity policy allows and what the boundary permits.

How Boundaries Actually Work

The evaluation logic is simple once you internalize it. AWS calculates effective permissions for any API call as follows:

  1. Check for explicit denies (kills the request immediately)
  2. Check SCPs — if the organization’s SCP doesn’t allow it, denied
  3. Check if either the identity policy or a resource-based policy (for same-account) allows it
  4. For identity policies, check that the permission boundary also allows it
  5. Check session policies if active

Step 4 is the boundary’s role. Even if the identity policy explicitly allows s3:DeleteBucket, if the boundary doesn’t also allow s3:DeleteBucket, the request is denied. The boundary functions as a filter over the identity policy.

The intersection means:

  • Identity policy allows s3:*, boundary allows s3:GetObject → effective: s3:GetObject only
  • Identity policy allows s3:GetObject, boundary allows s3:* → effective: s3:GetObject only
  • Identity policy allows s3:GetObject, boundary allows s3:GetObject → effective: s3:GetObject

All three cases produce the same result. The boundary is a ceiling, not a grant. The common mistake is thinking that a permissive boundary with a restrictive identity policy somehow inherits the boundary’s permissions — it doesn’t. The restrictive policy still limits the result.

The Privilege Escalation Problem Boundaries Solve

Without boundaries, any principal that can create IAM roles can escalate privileges. Here’s the sequence:

  1. Developer has iam:CreateRole, iam:AttachRolePolicy, iam:CreatePolicy
  2. Developer creates a new role with AdministratorAccess attached
  3. Developer assumes that role
  4. Developer now has full account access

Boundaries interrupt this at step 2. If the developer’s permissions include a boundary, any role they create must also have that same boundary (enforced by a condition in their policy). And since the boundary limits what they can do, the new role can’t exceed those limits either.

Set up a policy that forces developers to attach a boundary when creating roles:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowRoleCreation",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:AttachRolePolicy",
        "iam:DetachRolePolicy",
        "iam:PutRolePolicy",
        "iam:DeleteRolePolicy"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/AppTeamBoundary"
        }
      }
    },
    {
      "Sid": "AllowPassRoleWithBoundary",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/AppTeamBoundary"
        }
      }
    },
    {
      "Sid": "DenyBoundaryModification",
      "Effect": "Deny",
      "Action": [
        "iam:DeleteRolePermissionsBoundary",
        "iam:DeleteUserPermissionsBoundary",
        "iam:CreatePolicy",
        "iam:CreatePolicyVersion",
        "iam:DeletePolicy"
      ],
      "Resource": "arn:aws:iam::123456789012:policy/AppTeamBoundary"
    }
  ]
}

The first two statements allow role creation only when the AppTeamBoundary policy is set as the boundary. The third statement prevents the developer from modifying or deleting the boundary policy itself — closing the obvious escape hatch.

Creating and Attaching Boundaries

A boundary is a standard IAM managed policy. Create it like any other policy, then attach it during role creation:

# Create the boundary policy — defines what delegated roles can do at most
aws iam create-policy \
  --policy-name AppTeamBoundary \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "s3:*",
          "dynamodb:*",
          "lambda:*",
          "logs:*",
          "cloudwatch:*",
          "xray:*"
        ],
        "Resource": "*"
      }
    ]
  }'

# Create a role and set the boundary in a single command
aws iam create-role \
  --role-name my-lambda-role \
  --assume-role-policy-document file://trust-policy.json \
  --permissions-boundary arn:aws:iam::123456789012:policy/AppTeamBoundary

# Add the boundary to an existing role
aws iam put-role-permissions-boundary \
  --role-name my-lambda-role \
  --permissions-boundary arn:aws:iam::123456789012:policy/AppTeamBoundary

# Remove a boundary (requires permission to do so)
aws iam delete-role-permissions-boundary \
  --role-name my-lambda-role

With AppTeamBoundary in place, even if someone attaches AdministratorAccess to my-lambda-role, that role cannot call IAM APIs, EC2, RDS, or anything else outside the boundary’s allowed actions.

A Real Delegation Setup

Here’s a complete example: your platform team wants to let application teams manage their own Lambda execution roles without requiring a ticket for every new function.

Step 1 — Define the boundary policy. This represents the maximum permissions any app-team-created role can have:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query",
        "secretsmanager:GetSecretValue",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "xray:PutTraceSegments",
        "xray:PutTelemetryRecords"
      ],
      "Resource": "*"
    }
  ]
}

Step 2 — Create a developer policy that allows role management but only with the boundary enforced:

# The developer policy (attached to dev role or group)
aws iam create-policy \
  --policy-name AppTeamIAMDelegation \
  --policy-document file://delegation-policy.json

# Attach to the dev role
aws iam attach-role-policy \
  --role-name AppTeamRole \
  --policy-arn arn:aws:iam::123456789012:policy/AppTeamIAMDelegation

Step 3 — Developers create Lambda roles with the boundary:

# Developer creates a Lambda execution role
aws iam create-role \
  --role-name order-processor-lambda \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }]
  }' \
  --permissions-boundary arn:aws:iam::123456789012:policy/AppTeamBoundary

# Attach only what the function actually needs
aws iam attach-role-policy \
  --role-name order-processor-lambda \
  --policy-arn arn:aws:iam::123456789012:policy/OrderProcessorPermissions

The developer is free to attach any policy they want to order-processor-lambda. If they accidentally attach AdministratorAccess, the boundary limits the actual effective permissions to the app-team-approved list. No security team intervention needed.

Boundaries vs SCPs

Permission boundaries and Service Control Policies serve similar purposes but operate at different levels.

SCPs work at the AWS Organizations level. An SCP applied to an account restricts everything in that account — every identity, regardless of their individual policies or boundaries. SCPs are set by the management account and applied to OUs or member accounts. Developers inside a member account cannot modify SCPs. For the full setup, the AWS Organizations and Control Tower guide covers SCP application across accounts.

Permission boundaries work at the individual identity level inside a single account. They’re attached to a specific user or role, not to the account. An account admin can apply different boundaries to different teams. The AWS IAM roles and policies guide covers how SCPs and boundaries interact in the full policy evaluation chain — they stack, not replace.

The practical split: use SCPs to enforce account-wide guardrails that no developer should be able to override (deny iam:CreateUser, deny specific regions, deny root account usage). Use permission boundaries to delegate IAM administration within those guardrails without enabling privilege escalation.

What Boundaries Don’t Cover

Boundaries apply to identity-based policies. They don’t restrict resource-based policies.

If an S3 bucket policy allows s3:GetObject for a specific role, and that role has a permission boundary that doesn’t include s3:GetObject, the resource-based policy still allows it for same-account access. The boundary filtering doesn’t apply to resource policy grants in the same account.

For cross-account access, both the identity policy and the resource policy must allow the action, and the boundary must also allow it. All three gates must pass. Cross-account role assumption patterns are covered in detail in the IAM cross-account roles guide.

Boundaries also don’t restrict what services can do when acting on behalf of a principal. If a Lambda function’s execution role has a boundary, but the function’s code calls another AWS service that then performs actions using that service’s own role, the boundary on the Lambda role doesn’t affect the other service’s permissions.

Checking What Boundaries Are in Place

Audit which roles have boundaries set:

# List all roles with their permission boundaries
aws iam list-roles --query \
  'Roles[*].{Name:RoleName,Boundary:PermissionsBoundary.PermissionsBoundaryArn}' \
  --output table

# Check a specific role's boundary
aws iam get-role --role-name my-lambda-role \
  --query 'Role.PermissionsBoundary'

# Simulate what effective permissions a role has
# (useful for verifying boundary is working as expected)
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/order-processor-lambda \
  --action-names s3:GetObject dynamodb:DeleteTable \
  --resource-arns '*'

The simulate-principal-policy command is invaluable for testing boundary configurations without actually executing the calls. It tells you exactly which policy (identity, resource, boundary, SCP) is causing an allow or deny for a given action.

Common Mistakes

Forgetting to lock down the boundary policy itself. If a developer has iam:CreatePolicyVersion, they can add statements to the boundary policy, expanding its scope and their own effective permissions. The deny statement in the delegation policy above (blocking iam:CreatePolicy, iam:CreatePolicyVersion on the boundary ARN) prevents this.

Setting the boundary too broad. A boundary of *:* allows everything, which means the boundary provides no protection at all. Every action the identity policy allows will also pass through the boundary. This mistake usually happens when someone sets a permissive boundary “temporarily” and forgets about it.

Setting the boundary and forgetting to give the role actual permissions. Remember, boundaries don’t grant anything. A role with a permissive boundary and no identity-based policies still can’t do anything. Both sides of the intersection need to allow the action.

Not testing with iam:simulate-principal-policy. The intersection logic is non-obvious until you’ve debugged it a few times. Simulating before deploying is faster than debugging from an AccessDenied error in production.

Permission boundaries are the right tool when your organization wants to distribute IAM administration across teams without creating a privilege escalation risk. The setup involves more upfront design work than simply handing out IAM access, but the alternative — centralizing all role creation with a platform team bottleneck — doesn’t scale past a handful of application teams.

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus