Repository navigation
sec(iac/aws): role-prefix ARN scopes are satisfiable via the IAM path (arn:aws:iam::*:role/cudly-*) #197
Description
Surfaced during the review of LeanerCloud/cloud-commitments-cli#1818 (which closes LeanerCloud/cloud-commitments-cli#1723). Pre-existing, not introduced by that PR, which changed no Resource, Condition or ARN line. Filed separately rather than widening LeanerCloud/cloud-commitments-cli#1818's scope.
What
terraform/environments/aws/ci-cd-permissions/policy_data.tf scopes several statements with a trailing-wildcard role ARN:
:61arn:aws:iam::*:role/cudly-*(withpolicy/cudly-*andinstance-profile/cudly-*):77arn:aws:iam::*:role/cudly-*, theiam:PassRolestatement, conditioned oniam:PassedToService
In an IAM resource ARN, * matches any sequence of characters including /, and the IAM role path is part of the ARN. So arn:aws:iam::123456789012:role/cudly-x/EvilRole satisfies arn:aws:iam::*:role/cudly-*. The prefix that is supposed to identify our roles can be carried in the path, which is a field the role's creator controls, rather than in the role name.
The same shape applies to arn:aws:iam::*:role/CUDly* in CrossAccountAssumeRoleCeiling. That one additionally has drift protection from TestBoundaryMatchesCrossAccountRolePrefix, but the path-matching property is identical. Note this is a different concern from LeanerCloud/cloud-commitments-cli#1628 item 4 / LeanerCloud/cloud-commitments-cli#1636, which is about the missing per-tenant account condition on sts:AssumeRole, not about path-bearing ARNs.
Why it matters
This is the classic IAM path wildcard bypass: an attacker satisfies a name-prefix guard by putting the required prefix in a segment they choose.
It is currently contained, and the containment is worth stating precisely so nobody over-reacts or under-reacts:
- Creating any role requires
iam:CreateRole, which sec(iac/aws): deploy role can create an unbounded role and attach AdministratorAccess to it cloud-commitments-cli#1705 / sec(iac/aws): gate deploy-role IAM writes on a permissions boundary cloud-commitments-cli#1722 gated on the target carrying the permissions boundary. A role created atcudly-x/…is therefore itself boundaried. - The uncontained case is a
cudly-*role created out of band, by hand or in the console, by someone who already has IAM privileges.PassRoleCeiling's own comment already admits this residual.
So this is not a live escape today. It is a guard whose correctness depends entirely on a separate control staying in place, which is exactly the kind of coupling that breaks silently later.
Fix direction
Prefer a form that cannot be satisfied by a path segment. Options, roughly in order of preference:
- Constrain the path explicitly, e.g. require
arn:aws:iam::*:role/cudly-*and add aniam:ResourceTagor a path condition that pins the expected path, so the name prefix is not the only thing being checked. - Pin the path in the ARN itself (
role/cudly/…) and match on the full, expected shape rather than a trailing wildcard. - At minimum, add a guard test asserting that a path-bearing ARN such as
arn:aws:iam::123456789012:role/cudly-x/EvilRoledoes not satisfy the intended scope, so the residual is pinned rather than remembered.
Whatever is chosen, note the house rule: a security condition must be compared to its expected value. "Starts with the right prefix" is a shape check, and a shape check admits every value of that shape.
Verification bar
Both directions. Assert that a path-bearing impostor ARN is refused and that the real deploy roles still match, or the fix will pass its test by refusing everything and break the deploy.
Activity
sec(iac/aws): pin ecs, lambda and ssm per action in the deploy bounda…
sec(iac/aws): pin cross-account sts:AssumeRole to declared account IDs (
IAmJSD commentedon Oct 16, 2026
Claiming: working on this
fix(iac/aws): deny path-bearing cudly-* IAM ARNs in deploy policy and…