AWS Permission Boundaries: Delegated Administration Without Privilege Escalation
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 useAllowIAMWithBoundary— developers can create roles, but only if the same boundary is attached and the role name starts withapp-DenyBoundaryModification— prevents removing the boundary from any roleDenyEscalationPaths— 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
- Forgetting the self-referential constraint — if the boundary allows
iam:CreateRolewithout requiring the same boundary, developers can create roles without boundaries - Not blocking
iam:CreatePolicyVersion— a developer who can create policy versions can modify existing managed policies to grant themselves anything - Allowing
iam:PassRolewithout service conditions — lets developers pass overpermissive roles to themselves via Lambda or other services - 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.