← Back to Blog

AWS Permission Boundaries: Delegated Administration Without Privilege Escalation

5 min read

Permission boundaries solve one of the hardest problems in multi-team AWS environments: how do you let developers create IAM roles for their applications without giving them the ability to escalate their own privileges? Without boundaries, any user who can create a role can create one with AdministratorAccess and assume it.

How Permission Boundaries Work

A permission boundary is an IAM policy attached to a user or role that sets the maximum permissions. The effective permissions are the intersection of the identity-based policy and the boundary.

Effective permissions = Identity policy ∩ Permission boundary ∩ SCPs

If the identity policy grants s3:* but the boundary only allows s3:GetObject, the effective permission is s3:GetObject. The boundary cannot grant permissions — it can only restrict them.

Building a Permission Boundary

The Boundary Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowedServices",
      "Effect": "Allow",
      "Action": [
        "s3:*",
        "dynamodb:*",
        "lambda:*",
        "sqs:*",
        "sns:*",
        "logs:*",
        "cloudwatch:*",
        "xray:*",
        "events:*",
        "states:*",
        "execute-api:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowIAMWithBoundary",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:AttachRolePolicy",
        "iam:PutRolePolicy",
        "iam:PutRolePermissionsBoundary",
        "iam:TagRole",
        "iam:PassRole"
      ],
      "Resource": "arn:aws:iam::*:role/app-*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
        }
      }
    },
    {
      "Sid": "AllowReadOnlyIAM",
      "Effect": "Allow",
      "Action": [
        "iam:GetRole",
        "iam:GetPolicy",
        "iam:GetRolePolicy",
        "iam:ListRoles",
        "iam:ListPolicies",
        "iam:ListRolePolicies",
        "iam:ListAttachedRolePolicies"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyBoundaryModification",
      "Effect": "Deny",
      "Action": [
        "iam:DeleteRolePermissionsBoundary",
        "iam:DeleteUserPermissionsBoundary"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyEscalationPaths",
      "Effect": "Deny",
      "Action": [
        "iam:CreatePolicyVersion",
        "iam:SetDefaultPolicyVersion",
        "iam:CreateUser",
        "iam:CreateAccessKey",
        "organizations:*",
        "account:*"
      ],
      "Resource": "*"
    }
  ]
}

Key design decisions:

  • AllowedServices — lists exactly which AWS services developers can use
  • AllowIAMWithBoundary — developers can create roles, but only if the same boundary is attached and the role name starts with app-
  • DenyBoundaryModification — prevents removing the boundary from any role
  • DenyEscalationPaths — blocks common privilege escalation vectors like creating new policy versions or access keys

The Developer Policy

This policy grants developers the ability to create and manage their own application roles:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCreateAppRoles",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:AttachRolePolicy",
        "iam:DetachRolePolicy",
        "iam:PutRolePolicy",
        "iam:DeleteRolePolicy",
        "iam:PutRolePermissionsBoundary",
        "iam:TagRole",
        "iam:UntagRole"
      ],
      "Resource": "arn:aws:iam::*:role/app-*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
        }
      }
    },
    {
      "Sid": "AllowPassAppRoles",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::*:role/app-*",
      "Condition": {
        "StringEquals": {
          "iam:PassedToService": [
            "lambda.amazonaws.com",
            "ecs-tasks.amazonaws.com",
            "states.amazonaws.com"
          ]
        }
      }
    },
    {
      "Sid": "AllowAppServiceActions",
      "Effect": "Allow",
      "Action": [
        "lambda:*",
        "s3:*",
        "dynamodb:*",
        "sqs:*",
        "sns:*"
      ],
      "Resource": "*"
    }
  ]
}

The iam:PassedToService condition ensures developers can only assign roles to Lambda, ECS, and Step Functions — not to themselves or other IAM entities.

Testing Permission Boundaries

Programmatic Verification

import boto3

def test_boundary_enforcement(role_name, boundary_arn):
    """Verify that a permission boundary correctly restricts a role"""

    iam = boto3.client('iam')
    simulator = boto3.client('iam')

    # Actions that should be ALLOWED (within boundary)
    allowed_actions = [
        ('s3:GetObject', 'arn:aws:s3:::my-bucket/*'),
        ('dynamodb:PutItem', 'arn:aws:dynamodb:us-east-1:123456789012:table/MyTable'),
        ('lambda:InvokeFunction', 'arn:aws:lambda:us-east-1:123456789012:function:my-func'),
    ]

    # Actions that should be DENIED (outside boundary)
    denied_actions = [
        ('iam:CreateUser', 'arn:aws:iam::123456789012:user/*'),
        ('iam:DeleteRolePermissionsBoundary', f'arn:aws:iam::123456789012:role/{role_name}'),
        ('ec2:RunInstances', '*'),
        ('rds:CreateDBInstance', '*'),
    ]

    results = {'passed': 0, 'failed': 0, 'details': []}

    role_arn = f"arn:aws:iam::123456789012:role/{role_name}"

    for action, resource in allowed_actions:
        response = simulator.simulate_principal_policy(
            PolicySourceArn=role_arn,
            ActionNames=[action],
            ResourceArns=[resource]
        )
        decision = response['EvaluationResults'][0]['EvalDecision']
        passed = decision == 'allowed'
        results['passed' if passed else 'failed'] += 1
        results['details'].append({
            'action': action, 'expected': 'allowed',
            'actual': decision, 'passed': passed
        })

    for action, resource in denied_actions:
        response = simulator.simulate_principal_policy(
            PolicySourceArn=role_arn,
            ActionNames=[action],
            ResourceArns=[resource]
        )
        decision = response['EvaluationResults'][0]['EvalDecision']
        passed = decision in ('implicitDeny', 'explicitDeny')
        results['passed' if passed else 'failed'] += 1
        results['details'].append({
            'action': action, 'expected': 'denied',
            'actual': decision, 'passed': passed
        })

    return results

Run this test in CI/CD before deploying boundary policy changes. A boundary that is too permissive opens escalation paths; one that is too restrictive breaks applications.

Common Pitfalls

  1. Forgetting the self-referential constraint — if the boundary allows iam:CreateRole without requiring the same boundary, developers can create roles without boundaries
  2. Not blocking iam:CreatePolicyVersion — a developer who can create policy versions can modify existing managed policies to grant themselves anything
  3. Allowing iam:PassRole without service conditions — lets developers pass overpermissive roles to themselves via Lambda or other services
  4. Boundary too broad — a boundary that includes iam:* defeats the purpose entirely

Enforcing Permission Boundaries with AccessLens

Permission boundaries are powerful but fragile. A single misconfigured boundary policy can open privilege escalation paths that are difficult to detect through manual review.

AccessLens helps enforce permission boundaries by providing:

  • Boundary coverage analysis that identifies roles and users missing permission boundaries
  • Escalation path detection that finds roles capable of creating unbounded principals
  • Effective permission calculation that shows the actual permissions after boundary intersection
  • Drift monitoring that alerts when boundary policies are modified or removed

Permission boundaries only work when they are consistently applied and correctly configured. AccessLens provides continuous verification that your delegation model remains secure.

Verify your permission boundaries with AccessLens and ensure that delegated administration does not create privilege escalation opportunities.

Ready to secure your AWS environment?

Get comprehensive IAM visibility across all your AWS accounts in minutes.