Repository navigation
sec(iac): setup-gcp-wif.sh creates WIF providers with no attribute condition and grants pool-wide SA impersonation #1544
Description
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code.
Severity: CRITICAL. This template is deployed by customers into their own cloud accounts, so the blast radius is their infrastructure, not ours.
Two defects in one script, filed together because they share a fix session and a root cause: the shell path lacks the guard its Terraform sibling enforces.
Defect A: AWS mode creates the WIF provider with no attribute condition, then grants pool-wide impersonation
Where: arm/CUDly-CrossSubscription/setup-gcp-wif.sh:99-103 (provider create) and :133-139 (IAM binding)
What: the --provider-type aws branch calls gcloud iam workload-identity-pools providers create-aws with only --account-id, and never passes --attribute-condition. The script then binds roles/iam.workloadIdentityUser to principalSet://iam.googleapis.com/projects/<n>/locations/global/workloadIdentityPools/<pool>/*, i.e. every identity in the pool.
The Terraform sibling iac/federation/gcp-target/terraform/main.tf:137-147 carries an explicit precondition forbidding exactly this state, with the error text: "Without it the attribute_condition is null and any IAM role in the AWS account can federate as the GCP service account."
Failure scenario: a customer runs the documented AWS-mode command. Any IAM principal in the referenced AWS account (a developer's sandbox role, a CI runner, an EC2 instance profile on a compromised host, anything that can call sts:GetCallerIdentity) exchanges its AWS credentials at sts.googleapis.com and impersonates the customer's GCP service account, which holds compute.commitments.create/update (spend authority) or, in the impersonation bundle, commerceorgpolicy.commitmentAdmin. No CUDly involvement is required.
Fix: require a --aws-role-name argument in AWS mode and emit --attribute-condition="attribute.aws_role.contains('assumed-role/<role>/')", and narrow the binding to principalSet://.../attribute.aws_role/..., matching the Terraform module's mandatory precondition.
Defect B: OIDC mode warns instead of failing, and the documented usage omits the guard
Where: arm/CUDly-CrossSubscription/setup-gcp-wif.sh:110-126; documented usage example at :20-27
What: in OIDC mode, when --subject-condition is empty the script prints a WARNING to stderr and proceeds to create the provider with no --attribute-condition (:123-125 only appends the flag when the variable is non-empty). The header's copy-paste example uses --issuer-uri https://token.actions.githubusercontent.com and does not include --subject-condition.
Failure scenario: a customer copy-pastes the documented OIDC command verbatim. Because token.actions.githubusercontent.com is a globally shared public issuer, any GitHub Actions workflow in any repository on GitHub can request an OIDC token, exchange it through the customer's pool, and impersonate the customer's CUDly service account with commitment-purchase rights. A stderr warning on a set -euo pipefail script that still succeeds is not a control.
Fix: make --subject-condition mandatory for --provider-type oidc (exit 1 like the other required-arg checks at :59-74) and fix the header example to include it.
Related
Part of the onboarding-path drift class filed separately: the Terraform path is hardened and the shell/CFN/ARM path for the identical onboarding job is not.
Activity
cristim commentedon Jul 28, 2026
PR #1651 fixes both defects in
arm/CUDly-CrossSubscription/setup-gcp-wif.sh. It usesRefsrather thanClosesdeliberately: defect B's shape (a pool-wideprincipalSetimpersonation grant) also exists in the sibling Terraform onboarding path atiac/federation/gcp-target/terraform/main.tf:156-163, so auto-closing a p0 there would be premature. That is filed as #1661, along with a silent provider-create failure in the served onboarding template.If the intended scope of this issue is the shell script only, it can be closed when #1651 merges - both filed defects are fully fixed there.
cristim commentedon Jul 28, 2026
Correction: the framing premise of this issue is disproved
This issue's body frames both defects as "the shell path lacks the guard its Terraform sibling enforces", holding up
iac/federation/gcp-target/terraformas the safe reference implementation. That premise is wrong, and anyone reading this issue later should not treat the Terraform path as the good example. It is the worse of the two.The premise is half true: the Terraform module does enforce a non-null
attribute_conditionvialifecyclepreconditions, which is defect A. But:main.tf:162bindsroles/iam.workloadIdentityUsertoprincipalSet://.../workloadIdentityPools/<pool>/*on the AWS branch - the same pool-wide grant this issue reports.attribute.aws_role = "assertion.arn"raw and substring-tests it withcontains('assumed-role/<role>/'). An attacker with onlyiam:CreateUserin the trusted AWS account creates an IAM user whose path is/assumed-role/<role>/(legal under AWS's documented path regex(/)|(/[!-~]+/));GetCallerIdentitythen returns an ARN containing the very substring the condition tests for, and the pool-wide binding admits it. IAM role paths do not work here because STS strips them fromassumed-roleARNs - it is specifically the user ARN, which retains its path, that defeats the test.==, which is also why the fixed shell script is immune to the bypass above.Filed as #1667 (p0/critical), with the fix direction and a note that it changes
attribute_mappingon live deployments and so needs a migration note.One consequence worth flagging: the
pool_wide_grants()scan added tosetup-gcp-wif.shin #1651 flags a Terraform-created grant as legacy and refuses to report success. The two customer-facing onboarding paths now actively contradict each other until #1667 lands.