AWS IAM Permission Boundaries: Stop Privilege Escalation
Permission Boundaries are the IAM feature that lets you safely give developers iam:CreateRole without giving them admin access to the entire account. They are also the most misunderstood IAM feature in AWS. Most teams either ignore them or use them backwards. Here is what they actually do, the production pattern that uses them correctly, and the trap that breaks running apps if you get the policy wrong.
The 60-second mental model
A Permission Boundary is a maximum permissions cap attached to an IAM user or role. Even if you give that user an admin policy, the user cannot do anything outside the boundary.
Think of it as: "policies say what you CAN do; permission boundary says what you MIGHT BE ALLOWED TO do, no matter what your policies say."
Effective permissions = (attached policies) ∩ (permission boundary)
That intersection is the magic. A user with admin policies AND a permission boundary that only allows S3 actions ends up with read-write access to S3 only. Admin elsewhere is blocked by the boundary.
The privilege escalation problem they solve
Without permission boundaries, here is the most common AWS privilege escalation pattern:
- You have an IAM user with
iam:CreateRoleandiam:AttachRolePolicypermissions for "self-service" role creation. - That user creates a new role and attaches the AWS-managed
AdministratorAccesspolicy to it. - That user assumes the new role.
- That user is now an admin.
This is exactly the pattern that several real-world AWS breaches followed. A developer's IAM credentials got leaked, the credentials had limited permissions, but they had iam:CreateRole. The attacker created an admin role and escalated.
Permission Boundaries stop this. The user can only create roles that have a boundary equal to or stricter than the user's own boundary. The new role inherits a maximum permissions cap.
The minimum-viable production pattern
The pattern that stops the escalation above:
- Define a "DeveloperBoundary" managed policy. This is the maximum a developer's actions can ever do.
- Attach the DeveloperBoundary as the permissions boundary on every developer IAM user/role.
- Configure the developer's IAM policy so they can ONLY create roles where the boundary is also DeveloperBoundary. (Use the
iam:PermissionsBoundarycondition key.)
The result: developers can create roles, attach policies, build apps. They cannot create a role that escapes the boundary, even by attaching AdministratorAccess.
The actual policies (copy-paste ready)
The DeveloperBoundary itself: what is the maximum a developer can do? Common answer: anything within their own dev account, except IAM dangerous actions and account-level changes.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowMostServices",
"Effect": "Allow",
"Action": [
"s3:*", "ec2:*", "lambda:*", "dynamodb:*",
"logs:*", "cloudwatch:*", "events:*",
"rds:*", "sqs:*", "sns:*", "kms:*"
],
"Resource": "*"
},
{
"Sid": "AllowIAMScopedToBoundary",
"Effect": "Allow",
"Action": ["iam:CreateRole", "iam:AttachRolePolicy", "iam:DetachRolePolicy"],
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary":
"arn:aws:iam::ACCOUNT_ID:policy/DeveloperBoundary"
}
}
},
{
"Sid": "DenyDangerousIAM",
"Effect": "Deny",
"Action": [
"iam:DeleteRolePermissionsBoundary",
"iam:DeleteUserPermissionsBoundary",
"iam:CreateUser",
"iam:CreateAccessKey",
"iam:DeleteAccountPasswordPolicy",
"organizations:*",
"account:*"
],
"Resource": "*"
}
]
}
Then the developer's actual IAM policy attached to the user gives them broad permissions, but their boundary caps the effect.
The "create role with this boundary or fail" condition
The single most important condition in any boundary-based system:
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary":
"arn:aws:iam::ACCOUNT_ID:policy/DeveloperBoundary"
}
}
This forces every iam:CreateRole call to specify the boundary. If the developer tries to create a role without specifying a boundary (or specifying a different one), the call is denied. This is what makes the boundary unbreakable.
The trap: forgetting that boundaries also cap legitimate roles
Here is where teams break their own applications.
You attach a permission boundary to a service role used by Lambda. The Lambda's existing policy says "Action": "kms:Decrypt", "Resource": "*". The Lambda has been working for months.
Today you tighten the boundary policy and remove KMS permissions because "Lambda doesn't really need KMS, right?"
Lambda dies in production. Effective permissions = (Lambda's policy ALLOW kms:Decrypt) ∩ (boundary that does NOT allow kms:Decrypt) = denied. Even though the Lambda's own policy explicitly allows it.
Fix: when you change a permission boundary, you are changing the maximum cap for everything that uses it. Audit dependents before tightening a shared boundary. Use Access Analyzer to discover what permissions are actually being used.
How permission boundaries differ from SCPs and ACLs
| Mechanism | Scope | Stops |
|---|---|---|
| Permission Boundary | Single user or role | Privilege escalation by that principal |
| SCP (Service Control Policy) | Account or OU in AWS Organizations | Anything anyone in that account can do, including the root user |
| Resource Policy (S3 ACL, KMS key policy) | The resource itself | Cross-account or cross-principal access to the resource |
| Session Policy | Single AssumeRole call | Time-boxed restriction on a specific role assumption |
You typically combine these. SCPs at the org level set hard limits no one in the account can break. Permission Boundaries at the user/role level stop privilege escalation by individual principals. Resource policies protect specific resources. Session policies add per-session restrictions.
The "developer self-service" pattern that uses all three
The architecture that lets developers build freely without giving them keys to the kingdom:
- SCP at the dev account: deny
organizations:*,account:*, anything that could affect billing or other accounts. - Permission Boundary on developer users: cap their actions to within the dev account at a "developer-appropriate" max. They can create roles, but only with this boundary.
- Resource policies on critical resources: KMS keys for production data, S3 buckets containing customer data. Even if a dev manages to assume a role with KMS access, the key policy denies them.
Layered controls. Each one is a backup if the others fail.
Common mistakes that defeat the boundary
- Forgetting the iam:PermissionsBoundary condition: developers can create unbounded roles. Whole defense gone.
- Using a boundary that allows iam:DeleteRolePermissionsBoundary: developers can remove their own boundary. Whole defense gone.
- Boundary attached to the user but the user creates a service role for Lambda without a boundary: the Lambda role escapes the boundary. Use the condition above.
- Trying to use a boundary as the only IAM control: boundaries are a permissions cap, not a grant. You still need explicit allow policies; the boundary just limits what those policies can grant.
- Boundaries inherited across role chains incorrectly: if Role A assumes Role B, Role A's boundary does not apply to Role B's actions. Plan accordingly.
Production deployment checklist
- Define one DeveloperBoundary policy. Cover all "developer-appropriate" services. Explicitly deny privilege escalation actions.
- Define one ServiceBoundary policy. Cover all "service-appropriate" actions. Use this on application roles.
- Tag every IAM user and role with the boundary they require. Use AWS Config to audit compliance.
- Force
iam:PermissionsBoundaryas a condition on everyiam:CreateRoledevelopers can call. - Audit existing roles. Find any role missing a boundary and decide: should it have one?
- Monitor with CloudTrail and Access Analyzer for any attempt to remove or modify boundaries.
Share IAM access keys safely
Bootstrapping new IAM roles requires sharing access keys with team members. Share through zero-knowledge encryption with auto-expiring links instead of email or Slack.
Create Encrypted PasteThe bottom line
Permission Boundaries are the IAM feature most teams should be using and are not. They eliminate the most common privilege escalation pattern in AWS, they let you safely give developers iam:CreateRole, and they cost nothing. The setup is one boundary policy, one condition on a developer policy, and discipline about not loosening the boundary later. The payoff is dramatically reduced blast radius when (not if) developer credentials leak.
Related reading: Identity and Access Management Best Practices, AWS Security Checklist for Production, Leaked AWS Credentials Playbook, AWS IAM Access Denied Troubleshooting, and Zero Trust Security Implementation.