Cloud Security Wire
AWS Azure GCP RSS
AWS Hardening Guide

AWS IAM Permission Boundaries: The Privilege Escalation Control You're Probably Not Using

IAM permission boundaries cap the maximum permissions a principal can exercise, even if their identity policy grants more. They're the correct technical control for delegated administration and self-service IAM scenarios — and most AWS environments don't have them configured. Here's how they work and how to implement them.

By Cloud Security Wire · ·
#AWS#IAM#permission-boundaries#privilege-escalation#delegated-admin#least-privilege#iam-hardening#cloud-security#SCPs#iam-policy

The most common IAM privilege escalation path in AWS environments isn’t a zero-day. It’s an IAM user or role that’s been granted iam:CreateRole and iam:AttachRolePolicy — enough to create a new role with AdministratorAccess and assume it. Once an attacker has those two permissions, the entire account is compromised regardless of what the original principal’s policies say.

The standard defence is permission boundaries. They’ve existed since 2018, they’re documented in NIST and AWS security guidance, and the majority of AWS environments still don’t have them deployed in the places that matter.

What Permission Boundaries Actually Do

An IAM permission boundary is a managed policy that you attach to an IAM user or role as a boundary policy rather than an identity policy. The effective permissions for any action are the intersection of what the identity policy grants AND what the boundary policy allows. Having one without the other grants nothing.

The key property: a principal cannot grant permissions to other principals that exceed their own boundary. If a developer role has a boundary that excludes iam:* and sts:AssumeRole for admin roles, that developer cannot create a role with admin-equivalent access, even if their identity policy says "Effect": "Allow", "Action": "*", "Resource": "*".

This breaks the two most common IAM privilege escalation paths:

  1. CreateRole + AttachRolePolicy: Creating a new role with a permissive trust policy and attaching AdministratorAccess, then assuming it.
  2. CreatePolicyVersion: Creating a new version of an existing policy that grants admin permissions, setting it as the default version.

With a correctly scoped permission boundary, both paths are blocked at the iam:CreateRole and iam:CreatePolicyVersion steps because the resulting entity’s effective permissions would exceed what the boundary allows, making the creation operation fail.

The Scenario Where This Matters Most: Delegated Administration

The typical architecture that makes permission boundaries essential is delegated IAM administration. A central platform team manages the AWS account. Application teams need the ability to create and manage IAM roles for their services — Lambda execution roles, EC2 instance profiles, ECS task roles. You want them to self-serve without routing every IAM change through the platform team.

Without boundaries, giving an application team iam:CreateRole and iam:AttachRolePolicy is giving them a path to account compromise. With boundaries, you can delegate IAM creation safely.

The boundary policy for a delegated application team:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowApplicationPermissions",
      "Effect": "Allow",
      "Action": [
        "s3:*",
        "dynamodb:*",
        "sqs:*",
        "sns:*",
        "lambda:*",
        "ec2:Describe*",
        "logs:*",
        "xray:*",
        "secretsmanager:GetSecretValue",
        "ssm:GetParameter*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowPassRoleForAppServices",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::*:role/app-*",
      "Condition": {
        "StringEquals": {
          "iam:PassedToService": [
            "lambda.amazonaws.com",
            "ecs-tasks.amazonaws.com",
            "ec2.amazonaws.com"
          ]
        }
      }
    },
    {
      "Sid": "DenyPrivilegedIAMActions",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateGroup",
        "iam:DeleteBoundaryPolicy",
        "iam:PutUserPermissionsBoundary",
        "iam:PutRolePermissionsBoundary",
        "iam:DeleteUserPermissionsBoundary",
        "iam:DeleteRolePermissionsBoundary"
      ],
      "Resource": "*"
    }
  ]
}

The critical clauses are the deny statements at the end. The iam:DeleteBoundaryPolicy and the four Delete*PermissionsBoundary actions prevent the delegated team from removing the boundary constraint itself, which would otherwise be the obvious escalation path.

Requiring Boundaries on Role Creation

The companion to a boundary policy is an SCP or IAM condition that requires any newly created role to have a boundary attached. Without this, a delegated team simply creates roles without boundaries and the protection is ineffective.

Service Control Policy to enforce this at the organisational level:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequirePermissionBoundaryOnRoleCreation",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:PutRolePermissionsBoundary"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::*:policy/AppTeamBoundary"
        }
      }
    },
    {
      "Sid": "RequirePermissionBoundaryOnUserCreation",
      "Effect": "Deny",
      "Action": "iam:CreateUser",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::*:policy/AppTeamBoundary"
        }
      }
    }
  ]
}

Attach this SCP to OUs containing accounts with delegated IAM permissions. The result: any CreateRole call that doesn’t specify the boundary policy in the PermissionsBoundary parameter is denied by the SCP.

Auditing Your Current Exposure

Finding roles that lack boundaries and have IAM creation permissions is a two-step audit:

# Step 1: Find roles with potentially dangerous IAM permissions and no boundary
aws iam list-roles --query 'Roles[?PermissionsBoundary==null].RoleName' --output text | \
  tr '\t' '\n' | while read rolename; do
    # Check if this role has IAM privilege escalation capabilities
    aws iam simulate-principal-policy \
      --policy-source-arn "arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/$rolename" \
      --action-names "iam:CreateRole" "iam:AttachRolePolicy" "iam:CreatePolicy" \
      --query 'EvaluationResults[?EvalDecision==`allowed`].EvalActionName' \
      --output text 2>/dev/null | grep -q "iam:" && echo "UNPROTECTED: $rolename"
  done

# Step 2: Check for any existing boundaries and what they permit
aws iam list-roles --query 'Roles[?PermissionsBoundary!=null].[RoleName, PermissionsBoundary.PermissionsBoundaryArn]' \
  --output table

For a faster sweep using AWS Config, the managed rule iam-no-inline-policy-check combined with a custom Config rule for boundary presence is more practical at scale than per-role simulation.

Boundaries vs SCPs: The Right Tool for Each Job

Permission boundaries and Service Control Policies solve different problems. SCPs apply organisation-wide and can’t be more permissive than the account root’s policies — they’re controls set by the central cloud team. Permission boundaries are set per-principal, often by the team provisioning identities, and constrain that specific principal’s effective permissions.

The common mistake is treating them as alternatives. They’re complementary. SCPs prevent accounts from exceeding organisation-level limits. Permission boundaries prevent individual principals within an account from escalating beyond their intended scope. For delegated IAM scenarios, you need both.

Terraform for deploying a boundary policy and requiring its use:

resource "aws_iam_policy" "app_team_boundary" {
  name        = "AppTeamBoundary"
  description = "Permission boundary for application team roles - caps maximum permissions"
  policy      = data.aws_iam_policy_document.app_team_boundary.json
}

# Role creation with mandatory boundary
resource "aws_iam_role" "app_service_role" {
  name                 = "app-my-service-execution"
  assume_role_policy   = data.aws_iam_policy_document.assume_role.json
  
  # Boundary required - role cannot exercise permissions beyond this policy
  permissions_boundary = aws_iam_policy.app_team_boundary.arn

  tags = {
    Team        = "application"
    Environment = var.environment
    BoundaryApplied = "true"
  }
}

The tag serves as a queryable audit trail — a Config rule can flag any role tagged application that lacks the BoundaryApplied marker or whose boundary doesn’t match the expected ARN.

Where Boundaries Don’t Help

Permission boundaries constrain what a principal can do, not what they can be given. They don’t protect against cross-account role assumptions where the trusting account has looser controls. They also don’t apply to resource-based policies (S3 bucket policies, KMS key policies, SQS queue policies) — a principal can still be granted permissions through those mechanisms that exceed their identity policy plus boundary.

For complete coverage, boundaries are one layer of a defence-in-depth approach that also includes SCPs, resource-based policy guardrails, and — for the highest-risk environments — AWS IAM Access Analyzer to continuously identify policies that allow unintended cross-account access or privilege escalation paths.

← All Analysis Subscribe via RSS