Homeβ€Ί Blogβ€Ί AWS Architecture Series #36 β€” The ceiling you can hand to a product team…
AWS Architecture AWS Architecture Series

AWS Architecture Series #36 β€” The ceiling you can hand to a product team

A platform team wants product teams to create their own IAM roles without being able to create a role more powerful than themselves. Every attempt either blocks the teams entirely or leaves a route to full administrator, and nobody can articulate which of the two a given design is.

Verified against current vendor documentation on 29 August 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

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.

The requirement is a ceiling, and only one IAM construct is one

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.

Diagram: a permissions boundary as a set intersection β€” the identity-based policy grants a set of actions, the boundary caps a set and grants nothing, and effective permissions are the actions present in both; below, the four routes by which access arrives outside that intersection
The boundary is the right-hand set. It can remove an action from the result and can never add one. The four routes at the bottom are the ones that do not pass through this intersection at all.

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

1
Deny the delegate the ability to edit the boundary

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.

Concrete rule

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.

2
Treat iam:PassRole as part of the boundary design, not as an afterthought

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 trap

PassRole 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.

3
Do not rely on a boundary to stop cross-account resource access

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.

Decision signal

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.

4
Budget the boundary document before it becomes the architecture

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.

Implication

A 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
One genuine footgun, documented and easy to trip

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.

Boundary on the delegated platform principal
{
  "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.

Next in this series

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".

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent