Skip to content

sec(iac): setup-gcp-wif.sh creates WIF providers with no attribute condition and grants pool-wide SA impersonation #1544

Closed

Description

@cristim

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

  1. cristim commented on Jul 28, 2026

    @cristim
    MemberAuthor

    PR #1651 fixes both defects in arm/CUDly-CrossSubscription/setup-gcp-wif.sh. It uses Refs rather than Closes deliberately: defect B's shape (a pool-wide principalSet impersonation grant) also exists in the sibling Terraform onboarding path at iac/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.

  2. cristim commented on Jul 28, 2026

    @cristim
    MemberAuthor

    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/terraform as 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_condition via lifecycle preconditions, which is defect A. But:

    • Defect B is present in the Terraform path too. main.tf:162 binds roles/iam.workloadIdentityUser to principalSet://.../workloadIdentityPools/<pool>/* on the AWS branch - the same pool-wide grant this issue reports.
    • The Terraform path additionally has a working bypass the shell script never had. It maps attribute.aws_role = "assertion.arn" raw and substring-tests it with contains('assumed-role/<role>/'). An attacker with only iam:CreateUser in the trusted AWS account creates an IAM user whose path is /assumed-role/<role>/ (legal under AWS's documented path regex (/)|(/[!-~]+/)); GetCallerIdentity then 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 from assumed-role ARNs - it is specifically the user ARN, which retains its path, that defeats the test.
    • The Terraform module's inline comment asserting that exact-match "cannot work" for AWS is incorrect. PR fix(arm): pin GCP WIF attribute condition and narrow SA grant #1651 does exactly that, by normalising the assertion to the role ARN and comparing with ==, 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_mapping on live deployments and so needs a migration note.

    One consequence worth flagging: the pool_wide_grants() scan added to setup-gcp-wif.sh in #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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions