An application team wants to create its own IAM roles in an AWS account instead of filing tickets with the platform team, but must never be able to create a role with administrator permissions. How do you delegate iam:CreateRole safely, and what else must the delegation policy lock down?
answer
- condition on the created entity's cap
- the ARN of the boundary is the condition value
- allow the create, deny the removal
- also protect the ceiling document itself
- PassRole is the side door
basics
~20 sGrant the IAM write actions only with a StringEquals condition on the iam:PermissionsBoundary key naming your boundary policy, so every role they create carries the cap. Then explicitly deny removing or replacing that boundary, editing the boundary policy itself, and passing roles they should not.
solid answer
~40 sGive the team `iam:CreateRole`, `iam:PutRolePolicy` and `iam:AttachRolePolicy` scoped to a path or name prefix they own, each with `"Condition": {"StringEquals": {"iam:PermissionsBoundary": "<boundary-policy-arn>"}}`. That condition means a create or a policy attachment succeeds only when the target role carries exactly that boundary, so any role they mint is capped no matter what policy they write for it. Then close the escape hatches with explicit denies: `iam:DeleteRolePermissionsBoundary` and `iam:PutRolePermissionsBoundary`, any write to the boundary policy itself such as `iam:CreatePolicyVersion` and `iam:DeletePolicyVersion` on that ARN, and unconstrained `iam:PassRole`. Without those denies the team simply creates a capped role, strips the boundary, and assumes an admin.
code
json · 36 lines{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateBoundedRolesOnly",
"Effect": "Allow",
"Action": ["iam:CreateRole", "iam:AttachRolePolicy", "iam:PutRolePolicy"],
"Resource": "arn:aws:iam::111122223333:role/dev/*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/DevBoundary"
}
}
},
{
"Sid": "NeverRemoveTheBoundary",
"Effect": "Deny",
"Action": [
"iam:DeleteRolePermissionsBoundary",
"iam:PutRolePermissionsBoundary"
],
"Resource": "*"
},
{
"Sid": "NeverRewriteTheCeiling",
"Effect": "Deny",
"Action": [
"iam:CreatePolicyVersion",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:DeletePolicy"
],
"Resource": "arn:aws:iam::111122223333:policy/DevBoundary"
}
]
}go deeper
Recall that IAM write permissions are escalation-equivalent to admin, and that the boundary condition key is what ties a delegated create to a mandatory cap.
Explain the policy mechanics: a StringEquals condition on iam:PermissionsBoundary across the create and policy-write actions, plus resource scoping to a path the team owns.
Demonstrate that you hunt the escape hatches — stripping the boundary, rewriting the boundary policy, passing a privileged role — and that you test the escalations rather than reasoning about them.
Own the tradeoff: this converts per-policy review into one carefully reviewed ceiling. Argue when that is worth it versus separating teams into their own accounts, and who owns the boundary long term.
## The problem IAM write permissions are the sharpest privilege-escalation surface in an AWS account. `iam:CreateRole` plus `iam:AttachRolePolicy` is functionally equivalent to `AdministratorAccess`: create a role, attach the admin policy, write a trust policy naming yourself, assume it. Any delegation of self-service IAM has to solve that problem or it is not a delegation, it is a grant of admin with extra steps. Permissions boundaries exist for exactly this case. The trick is that a boundary is a property of the created entity, and IAM exposes a condition key — `iam:PermissionsBoundary` — whose value is the ARN of the boundary attached to the entity the request is acting on. That lets the delegation policy demand: *you may perform this IAM write, but only against a role that carries this specific boundary.* ## The delegation policy, in three parts **1. Allow the creates and the policy writes, conditioned on the boundary.** ```json { "Effect": "Allow", "Action": ["iam:CreateRole", "iam:AttachRolePolicy", "iam:PutRolePolicy"], "Resource": "arn:aws:iam::111122223333:role/dev/*", "Condition": { "StringEquals": { "iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/DevBoundary" } } } ``` Two constraints are doing work here. The `Resource` limits them to a path they own, so they cannot rewrite the platform team's roles. The condition means a `CreateRole` without `--permissions-boundary arn:.../DevBoundary` is denied outright, and an `AttachRolePolicy` against a role that lacks that boundary is denied too — so they cannot bolt an admin policy onto some pre-existing unbounded role. **2. Deny anything that removes or weakens the cap.** ```json { "Effect": "Deny", "Action": [ "iam:DeleteRolePermissionsBoundary", "iam:PutRolePermissionsBoundary", "iam:DeleteUserPermissionsBoundary", "iam:PutUserPermissionsBoundary" ], "Resource": "*" } ``` This is the step teams forget, and it is the whole ballgame. Without it, the flow is: create a role with the boundary (allowed), then call `DeleteRolePermissionsBoundary` on the role you just created (also allowed, because nothing said otherwise), then attach admin. An explicit `Deny` closes it, and explicit denies cannot be overridden. **3. Protect the boundary policy document itself, and PassRole.** If the team can call `iam:CreatePolicyVersion` on the boundary policy's ARN, they can simply rewrite the ceiling to `"Action": "*"`. Deny writes to that ARN — `iam:CreatePolicyVersion`, `iam:DeletePolicyVersion`, `iam:SetDefaultPolicyVersion`, `iam:DeletePolicy`. `iam:PassRole` deserves its own thought. Creating a bounded role is safe; *handing an existing powerful role to a service* is not. If the team can pass an unrelated admin role to a Lambda function or an EC2 instance they control, the boundary on their own roles is irrelevant — they run code as that role instead. Scope `iam:PassRole` to the same path, and use the `iam:PassedToService` condition key to restrict which service may receive it. ## Writing the boundary itself The boundary is a normal policy document, but written as a ceiling: broad enough that teams rarely hit it for legitimate work, narrow enough to be meaningful. Typically it allows the application services the team needs (S3, DynamoDB, SQS, CloudWatch Logs) and pointedly omits the escalation surfaces — IAM writes, Organizations, account-level settings, key policy changes. Some teams add an explicit `Deny` inside the boundary for a small set of never-allowed actions, so the cap is legible even to someone skimming. Beware the temptation to make the boundary itself allow IAM writes "so the roles can manage themselves" — that reopens the hole one level down, because a bounded role that can create roles inherits the same problem unless the condition is applied there too. ## Verifying it Do not ship this on reasoning alone. Try the attacks: create a role without the boundary (expect denied), create one with it and try to strip the boundary (denied), attach `AdministratorAccess` to a bounded role and confirm the role still cannot call anything outside the cap (allowed to attach, useless in practice), and try to pass a privileged role to a service. The IAM policy simulator covers the static evaluation; a scratch account covers the rest. Pair it with monitoring on IAM write events so an unexpected boundary change is visible. ## Why this is the right shape The alternative — a central team reviewing every policy — does not scale and creates a queue that teams route around. The boundary shape moves the review from *every policy* to *one ceiling document*, reviewed rarely and carefully. That is the real argument to make in an interview: the mechanism converts an unbounded review burden into a bounded one.
- Why is it not enough to condition only iam:CreateRole on the boundary?Because the team can then act on roles they did not create. Conditioning `iam:AttachRolePolicy` and `iam:PutRolePolicy` on the same key means a policy write only succeeds against a role that already carries the boundary, which blocks attaching an admin policy to some pre-existing unbounded role in the account.
- How does iam:PassRole undermine this design if you leave it open?Creating bounded roles is safe, but passing an existing privileged role to a compute service is not. With unconstrained `iam:PassRole`, the team attaches a powerful role to a Lambda function or EC2 instance they control and executes as it. Scope PassRole to their own path and constrain the receiving service with `iam:PassedToService`.
- How do you keep the boundary from blocking legitimate work without widening it every week?Write it as a service-level allowlist rather than an action-level one — whole services the team legitimately uses — and omit only the escalation surfaces. Then track denials caused by the boundary so widening is evidence-driven, and review changes to that one document carefully, since it is now the only review gate left.
- How would you verify the delegation before handing it to a team?Attempt the escalations in a scratch account: create a role with no boundary, strip a boundary from a role you created, rewrite the boundary policy, and pass a privileged role to a service. Each must be denied. The policy simulator covers the static evaluation; the live attempts catch the action you forgot to name.
saying these in an interview costs you the question
- Grants iam:CreateRole with only a name prefix restriction
- Forgets to deny DeleteRolePermissionsBoundary
- Leaves the boundary policy itself writable by the team
- Ignores iam:PassRole as an escalation path
- Assumes the boundary applies to roles created without it