Business Challenge
A platform team owns eighty AWS accounts and is the bottleneck in all of them. Every Lambda function, every ECS task and every CI pipeline needs an IAM role, and every one of those roles arrives as a ticket. The queue is four days long, and the team is four people.
The obvious fix is to let product teams create their own roles. The obvious objection is equally clear: a principal that can create a role and attach AdministratorAccess to it, then assume that role, is an administrator. Granting iam:CreateRole and iam:AttachRolePolicy is functionally granting everything, by a route that takes about ninety seconds.
So the team tries to write the constraint as an identity policy. They enumerate the managed policies a product team may attach, and it breaks within a week because teams need customer-managed policies too. They try denying iam:* on anything outside a naming prefix, and teams work around it by naming things to fit. They try an SCP, which applies to every principal in the account including the platform team's own automation, and roll it back the same afternoon.
Each attempt is trying to express "you may create roles, but not roles more powerful than you" using a mechanism that has no notion of more powerful than. There is a mechanism that does, and it is not an identity policy, an SCP or a naming convention.
A permissions boundary sets the maximum permissions an identity-based policy can grant to an entity. It is the only mechanism in IAM that lets one principal define the upper bound of another principal it has not met yet. Everything else in the list above is a filter on the *creation call*, which is why every one of them was routed around by changing the call.
Architecture
AWS's definition is worth reading as a sentence about sets rather than about permissions: a permissions boundary is an advanced feature for using a managed policy to set the maximum permissions that an identity-based policy can grant to an IAM entity. Two sets are involved, and only one of them can put anything into the answer.
The intersection, stated precisely
AWS is explicit that an entity's permissions boundary allows it to perform only the actions that are allowed by both its identity-based policies and its permissions boundaries, and that the effective permissions are the intersection of both policy types. An explicit deny in either one overrides the allow, which makes three outcomes possible and only one of them an allow.
The consequence teams keep rediscovering is the one AWS states directly: when you use a policy to set the permissions boundary for a user, it limits the user's permissions but does not provide permissions on its own. A boundary allowing s3:* gives nobody S3 access. Attach it to an entity with no identity policy and that entity can do nothing at all, which is the correct behaviour and reliably reads as a bug the first time it happens.
Delegation is two boundaries, not one
The pattern that solves the ticket queue needs a boundary in two places, and this is the part that is usually built with only one.
The boundary the product team must apply
A managed policy defining the maximum any product-team-created role may have β the services in scope, plus explicit denies on the things that must never be reachable regardless of what anyone attaches. This is the ceiling on roles that do not exist yet.
The boundary on the product team itself
A second managed policy, set as the boundary of the delegated principal, whose allow on iam:CreateRole carries a iam:PermissionsBoundary condition. Without it the delegate can create roles freely and the first policy is decoration.
The condition key is what turns a convention into a control. An allow on iam:CreateRole that is conditional on iam:PermissionsBoundary equalling a specific policy ARN means the API call fails unless the boundary is attached at creation. There is no code path in which a role is created first and bounded afterwards, so there is no window.
Where the boundary sits relative to everything else
A boundary does not replace organisation-level controls and does not compete with them. AWS's wording for the three-way case is unambiguous: an IAM entity can make a request that is affected by an SCP, a permissions boundary, and an identity-based policy, and the request is allowed only if all three policy types allow it. Each is an independent narrowing, and the SCP is the one that also covers principals no delegation scheme created.
This is why "we use boundaries" and "we use SCPs" are not alternative answers to the same question. The SCP bounds the account. The boundary bounds one entity, and is the only one of the two that a platform team can hand to a product team as a self-service contract.
Why This Architecture Holds Up
It moves the review from the ticket to the policy
Reviewing eighty roles a month is unbounded work whose quality decays with volume. Reviewing one boundary policy is bounded work done once, revisited when the ceiling needs to change. The four-day queue disappears because nothing in the common path requires a human, and the security property is stronger than the one the queue was producing β a reviewer approving the fortieth role of the week is not meaningfully checking it.
It fails closed, loudly
Most access controls fail open in their interesting cases: a missing statement grants nothing but also blocks nothing that another statement already allowed. A boundary is an intersection, so a missing entry in the boundary removes the permission entirely. The failure is a denied API call on the product team's first deployment, not a quiet excess of privilege discovered in an audit eighteen months later.
The boundary is versioned, diffable and attributable
Because the ceiling is a customer managed policy, it is a document in a repository with a change history and a reviewer. "What could this team do in March" is answerable from a policy version rather than reconstructed from the union of whatever was attached at the time. Customer managed policies per account defaults to 1500, with a maximum quota of 10000, so a boundary per team or per workload class is well within reach even in a busy account.
It composes with the evaluation order rather than fighting it
The boundary is one of the narrowing gates in AWS's evaluation pipeline, and it behaves exactly as its position predicts β it cannot grant, and it is evaluated after the two gates that can. Nothing about it is special-cased. That is also why its limits are predictable rather than surprising, once you know what it bounds: identity-based policy, and only identity-based policy.
Key Architecture Decisions
The boundary is a managed policy, and a managed policy has versions. A delegate holding iam:CreatePolicyVersion and iam:SetDefaultPolicyVersion on the boundary policy can publish a version allowing everything and make it the default. Their ceiling is then whatever they chose, and the audit trail shows a routine policy update.
The delegate's own boundary carries an explicit deny on iam:CreatePolicyVersion, iam:DeletePolicy, iam:DeletePolicyVersion and iam:SetDefaultPolicyVersion for both boundary policy ARNs β the one they apply to others and their own β plus a deny on iam:DeleteRolePermissionsBoundary. A deny is used rather than an omitted allow because an explicit deny survives any policy anyone later attaches.
A bounded principal that can pass a role to a service is not constrained by its own boundary in what that service then does. AWS states the risk plainly: when setting the PassRole permission, you should make sure that a user does not pass a role where the role has more permissions than you want the user to have. The service assumes the role and acts with the role's permissions; the passer's ceiling is not in the evaluation at all.
The trapPassRole is not an API call. PassRole is a permission, meaning no CloudTrail logs are generated for IAM PassRole. There is no event to alert on β the evidence is inside the CreateFunction or RunInstances record that received the role. Scope iam:PassRole by resource ARN prefix, and narrow further with the iam:PassedToService condition key, which can be used to specify the service principal of the service to which a role can be passed.
The boundary bounds identity-based policy. AWS is direct that implicit denies in permissions boundaries do not limit resource-based policies, and the distinction turns on one detail of the Principal element. Resource-based policies that grant permissions to an IAM role ARN are limited by an implicit deny in a permissions boundary or session policy. But permissions granted directly to a session are not limited by an implicit deny in an identity-based policy, a permissions boundary, or session policy.
If the requirement is "this data must not be reachable by that team", write it as an explicit deny β in the boundary, or better in an SCP or RCP where it also covers principals outside the delegation scheme. An implicit deny, which is what a boundary that simply omits the action produces, is exactly the case a resource policy can route around.
A boundary must be a managed policy, and the size of each managed policy can't exceed 6,144 characters. An inline role policy gets 10,240 characters β so the ceiling document has exactly 60 percent of the budget of the thing it is bounding. Whitespace is not counted, which helps less than it sounds once the policy carries per-resource ARNs.
ImplicationA boundary written as an enumeration of permitted actions runs out of room in a real platform. Write it as broad service-level allows plus a short list of explicit denies for the things that must never be reachable β the deny list is the part that carries the security value, and it is short. Enumerating allows spends the character budget on the half of the document that only has to be approximately right.
What a boundary does and does not reach
| Access arrives via | Bounded? | Why | What to use instead |
|---|---|---|---|
| Identity-based policy on the entity | Yes | This is the set the boundary intersects with | Nothing β this is the design working |
| Resource policy naming the role ARN | Yes | Limited by an implicit deny in the boundary | Nothing further needed |
| Resource policy naming the session ARN | No | Granted directly to the session | Explicit deny in an SCP or RCP |
| A role passed to a service | No | The service acts with the passed role's permissions | Scoped iam:PassRole plus iam:PassedToService |
| A service-linked role | No | You cannot apply a permissions boundary to a service-linked role | SCP on the service's actions |
| Editing the boundary policy itself | No | The document is an ordinary managed policy with versions | Explicit deny on the policy ARNs |
Do not write a resource-based policy that combines NotPrincipal with a Deny effect while any of the intended principals carry a boundary. The NotPrincipal element with a Deny effect will always deny any IAM principal that has a permissions boundary policy attached, whatever the element lists. AWS recommends ArnNotEquals on aws:PrincipalArn instead. Rolling out boundaries can therefore break access on resources whose policies nobody touched.
Reference: the delegate's boundary
This is the second of the two policies β the one attached as the boundary of the principal doing the delegating. The first statement is what makes self-service role creation safe; the second and third are what stop the delegate from removing the first.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateRolesOnlyWithBoundary",
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"iam:DetachRolePolicy",
"iam:DeleteRolePolicy",
"iam:PutRolePermissionsBoundary"
],
"Resource": "arn:aws:iam::111122223333:role/app/*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary":
"arn:aws:iam::111122223333:policy/ProductTeamCeiling"
}
}
},
{
"Sid": "NoEditingEitherBoundary",
"Effect": "Deny",
"Action": [
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion"
],
"Resource": [
"arn:aws:iam::111122223333:policy/ProductTeamCeiling",
"arn:aws:iam::111122223333:policy/PlatformDelegateCeiling"
]
},
{
"Sid": "NoRemovingBoundaries",
"Effect": "Deny",
"Action": "iam:DeleteRolePermissionsBoundary",
"Resource": "*"
},
{
"Sid": "PassOnlyBoundedRolesToCompute",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::111122223333:role/app/*",
"Condition": {
"StringEquals": {
"iam:PassedToService": [
"lambda.amazonaws.com",
"ecs-tasks.amazonaws.com"
]
}
}
}
]
}
Note what this document is not doing: it grants the delegate nothing. It is a boundary, so every statement above only survives if a separate identity-based policy grants the same action. The Allow statements are the ceiling's shape, not permissions β which is the single sentence worth carrying away from the whole model.
Closing Thought
The permissions boundary is the only construct in IAM that lets one team define the upper bound of a principal that does not exist yet. That is a genuinely different capability from anything an identity policy offers, and it is what makes self-service infrastructure possible without making everyone an administrator by a ninety-second route.
It earns that by being narrow. It bounds identity-based policy and nothing else, so a resource policy naming a session, a role handed to a service, a service-linked role and the boundary document itself all sit outside it. None of those is a defect β each is the documented consequence of what a boundary is β but a design that assumes the boundary is a general-purpose ceiling has four gaps in it, and they are the four that get found.
The test to apply to any boundary you write: for each way this principal can cause an action to happen, does that action pass through an identity-based policy attached to this entity? Where the answer is yes, the boundary holds. Where it is no β a passed role, a session named in a bucket policy, a service acting on its own linked role β the control has to be an explicit deny somewhere the boundary does not reach.
Security & Identity β the role trust policy: why a role has two halves, what the Principal element actually delegates when it names an account, and the conditions that turn "this vendor can assume the role" into something narrower than "anyone in that vendor's account can".
Official AWS Reference
- AWS Documentation β Permissions boundaries for IAM entities
- AWS Documentation β Grant a user permissions to pass a role to an AWS service
- AWS Documentation β IAM roles: terms and concepts
- AWS Documentation β IAM and AWS STS quotas
- AWS Documentation β Policy evaluation logic
- AWS Documentation β AWS JSON policy elements: NotPrincipal
Comments