Repository navigation
sec(iac): GCP WIF Terraform bypass - IAM user path defeats contains() role check, plus pool-wide grant #1667
Description
Activity
- addedtriagedItem has been triagedItem has been triagedpriority/p0Drop everything; same-day fixDrop everything; same-day fixseverity/criticalMajor harm when it happensMajor harm when it happensurgency/nowDrop other thingsDrop other thingsimpact/all-usersAffects every userAffects every usereffort/mDaysDaystype/securitySecurity findingSecurity finding
on Jul 28, 2026 Closed by PR #1675 — verification record
Auto-closed by the
Closeskeyword; recording what was verified, since the merge rested on this rather than on CI.Note on the CodeRabbit signal: #1675's
CodeRabbitstatus readsuccesswhile itsdescriptionsaid "Review rate limited" — it never actually reviewed this p0 customer-facing PR, which presented as "0 unresolved threads". The gate here was an independent adversarial review.The fix is exact, and no bypass was found. The condition is
==against a normalised value. For it to match, the raw ARN must literally begin witharn:aws:sts::<pinned acct>:assumed-role/— and theiamvsstsservice segment is not attacker-controllable at any path depth. So this issue's exploit (an IAM user at path/assumed-role/CUDly-Execution/) normalises toarn:aws:iam::<acct>:user/assumed-role/CUDly-Executionand is rejected. A longer sibling role name (CUDly-Execution-Admin) is rejected by==.Non-obvious point worth preserving: Google documents two AWS mapping variants, and the simpler
extract('assumed-role/{role_name}/')form would still be vulnerable to this exact attack. The fix picked the full-ARN ternary variant.Both halves landed. The pool-wide
principalSet://.../*binding was genuinely narrowed to the documented exact-attribute-value form, not just the condition tightened. That also resolved an open question the PR had flagged: Google's docs show the ARN unencoded inside the identifier, so no URL-encoding is needed.Migration is a genuine in-place update, confirmed from provider source at three versions rather than registry prose:
attribute_mappingandattribute_conditionhave no ForceNew and both enter the updateMask. Not a breaking change.Tests were mutation-tested: four distinct mutations of
main.tfare each caught by a distinct test.Known residue, not blocking
depends_onandcreate_before_destroyorder the API calls but cannot order GCP's eventual consistency, so an existing customer re-applying may see a transient federation gap of minutes. Self-healing; no Terraform-side fix exists. A two-step-targetapply removes the window entirely and is worth documenting.Two deliberate narrowings: the pinned ARN hardcodes
arn:aws:, so GovCloud/China callers now fail closed; and the account is pinned in the condition (already enforced independently by the provider'saws { account_id }block).Cross-PR interaction caught before shipping
The Terraform module mapped a third attribute (
attribute.account) thatsetup-gcp-wif.shdoes not. Once #1673 added exact-set mapping validation, that would have hard-died on every Terraform-onboarded customer — with a wrong verdict and a destructive suggested remedy (delete a live provider). Resolved by dropping the unreferenced attribute; the two mapping sets are now byte-identical, verified bycmp.
Severity: CRITICAL. Like #1544, this is customer-facing:
iac/embed.go:9embedsfederation, andinternal/api/handler_federation.go:641-655returnsfederation/gcp-targetas the default bundle fortarget=gcp, so the zip customers download shipsmain.tfwith a pre-filled tfvars. Same endpoint, same customers, same defect class as #1544 - and more exploitable than the bug #1544 reported, because this path has a working bypass rather than just a missing second gate.#1544's framing said "the shell path lacks the guard its Terraform sibling enforces". That premise is wrong, and PR #1651 disproves it. The Terraform path is the worse of the two.
(a) Live bypass: raw
assertion.arn+ substring test - HEADLINEiac/federation/gcp-target/terraform/main.tf:102maps the attribute raw:and
:109substring-tests it:"attribute.aws_role.contains('assumed-role/${var.aws_role_name}/')"With
aws_role_name = "CUDly-Execution", an attacker who has onlyiam:CreateUserin the customer's AWS account:/assumed-role/CUDly-Execution/. AWS's documented path regex is(/)|(/[!-~]+/)- every character here is printable ASCII, so the path is legal.sts:GetCallerIdentityreturnsarn:aws:iam::<acct>:user/assumed-role/CUDly-Execution/evil.account_idcheck passes (same account).contains('assumed-role/CUDly-Execution/')is true - the crafted path supplies the substring the test looks for.Result: full impersonation of the CUDly GCP service account, which holds
compute.commitments.create/update.iam:CreateUserescalated into cross-cloud GCP spend authority.IAM role paths do not work, because STS drops the path from
assumed-roleARNs. It is specifically the IAM user ARN, which retains its path, that defeats the substring test.The shell script fixed in #1651 is immune, and the reason is the argument for the fix shape: it normalises the assertion down to the role ARN and compares with
==, so a user ARN never equalsarn:aws:sts::<acct>:assumed-role/<role>regardless of its path.(b) Pool-wide impersonation grant
main.tf:162:Byte-identical in shape to the grant removed in #1651. Its inline comment - "wildcard principalSet is intentional; session ARNs include variable session names so exact-match principalSet cannot work" - is precisely the claim #1651 disproves.
Worth noting how far the two customer-facing paths have now diverged: the
pool_wide_grants()scan added tosetup-gcp-wif.shin #1651 flags a Terraform-created grant as legacy and refuses to report success. A customer who onboards with Terraform and later runs the shell script is told, correctly, that their setup is unsafe.(c) No validation on the CEL-interpolated variables
variables.tf:57-67: neitheraws_role_namenoroidc_subjecthas avalidationblock, and both are interpolated into a CEL string literal.aws_role_name = "x') || true || ('"renders an always-true condition. This is the CEL break-out thatvalidate_principal_value()exists to stop on the shell side.(d) Stale "Recommended" wording
internal/iacfiles/templates/gcp-wif.tfvars.tmplshipsaws_role_namecommented out and labelled "Recommended", whilemain.tf:139-142requires it. A downloaded bundle failsterraform apply. Fails closed, but the wording misleads.Fix direction
Mirror PR #1651:
plus
validationblocks on both string vars rejecting',",\, backtick,*and$, and a fix to (d).This changes
attribute_mappingon live deployments, which existing customers federate against, so it needs its own migration note rather than a silent edit.Related: #1544 (same defect class, shell path, fixed in #1651), #1661 (adjacent gaps in the served CLI template).