Back to blog
Best PracticesCloud SecurityGCPIdentity & AccessOperations & Compliance

Workload Identity Provider Has No Conditions: Locking Down GCP Federation

A GCP workload identity provider without attribute conditions accepts any token from its issuer. Learn the risk and how to scope federation safely.

TL;DR

A workload identity pool provider without attribute conditions will accept any token that matches its issuer, letting external or unintended identities exchange credentials for GCP access. Add an --attribute-condition that restricts which external identities can authenticate, scoped to the exact repository, account, or subject you trust.

Workload Identity Federation is one of the best things to happen to GCP access management in years. It lets workloads running outside Google Cloud, like GitHub Actions runners, AWS Lambdas, or on-prem services, authenticate to GCP without long-lived service account keys. No more JSON key files leaking through committed config or Slack messages.

But the security model only holds if you tell GCP which external identities you actually trust. This check fires when a workload identity pool provider has no attribute conditions set, which means the provider will hand out access to anyone who shows up with a valid token from the configured issuer. That is a much wider door than most teams realize they left open.


What this check detects

Lensix flags any workload identity pool provider in your GCP project where the attributeCondition field is empty. Specifically, the iam_workloadidentitynocondition check inspects providers under your workload identity pools and reports those with no CEL-based condition controlling token acceptance.

A provider with no condition trusts the issuer and nothing else. If the provider is configured to trust https://token.actions.githubusercontent.com, then every GitHub Actions workflow on GitHub, in any repository owned by anyone, can present a valid token. Whether that token is allowed to impersonate one of your service accounts then comes down purely to your IAM bindings, which are often broader than people think.

Note: An attribute condition is a CEL (Common Expression Language) expression evaluated against the mapped attributes of an incoming token. If the expression returns false, the token exchange is rejected before any IAM binding is even considered. It is your first and most important filter.


Why it matters

The classic attack here involves GitHub OIDC. Say you set up a provider trusting GitHub's issuer and you bind it so that the federated identity can impersonate a service account with deploy permissions. If your IAM binding uses a broad principal like principalSet://...workloadIdentityPools/.../attribute.repository_owner/my-org but you skipped the attribute condition, an attacker who can run a workflow under any repo in my-org (or worse, in some misconfigurations, any repo at all) can mint GCP credentials.

Here is how the failure chains together:

  1. The provider has no attribute condition, so it accepts any token GitHub signs.
  2. An attacker forks a public repo, opens a pull request, or compromises a low-trust repo in your org.
  3. Their workflow requests an OIDC token from GitHub and exchanges it at your provider.
  4. If your IAM bindings are even slightly too generous, they now hold a short-lived GCP access token.

Danger: A provider with no condition combined with a wildcard or overly broad principalSet IAM binding is effectively a public credential vending machine. Treat this as a critical finding and remediate before your next deploy.

The business impact is the same as a leaked service account key, except harder to spot because there is no key file to find. The audit trail shows a legitimate-looking federated identity authenticating, and the exchange happens entirely through Google's STS endpoint. By the time anyone notices unusual API calls, the attacker has had a working credential for the full token lifetime.


How to fix it

The fix is to add an attribute condition that restricts which incoming tokens are accepted. You will usually scope this to a specific repository, an organization plus repository pattern, a specific AWS account, or whatever subject identifies the workload you actually trust.

Step 1: Inspect the current provider

List your pools and providers so you know exactly what you are working with.

gcloud iam workload-identity-pools list \
  --location="global" \
  --format="value(name)"

gcloud iam workload-identity-pools providers describe PROVIDER_ID \
  --location="global" \
  --workload-identity-pool="POOL_ID" \
  --format="yaml(attributeCondition, attributeMapping, oidc.issuerUri)"

If attributeCondition comes back empty, this provider is what the check flagged.

Step 2: Decide what to trust

Look at your attributeMapping first, because your condition can only reference attributes you have actually mapped. A typical GitHub mapping looks like this:

--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref"

Warning: You can only write conditions against attributes present in your mapping. If you want to restrict by branch (assertion.ref) but never mapped it, add it to the mapping first. Updating the mapping does not affect already-issued tokens but it does change what future tokens carry.

Step 3: Apply the attribute condition

Restrict to a single repository on the main branch:

gcloud iam workload-identity-pools providers update-oidc PROVIDER_ID \
  --location="global" \
  --workload-identity-pool="POOL_ID" \
  --attribute-condition="assertion.repository == 'my-org/my-repo' && assertion.ref == 'refs/heads/main'"

For an entire org, scope to the owner but be deliberate about it:

gcloud iam workload-identity-pools providers update-oidc PROVIDER_ID \
  --location="global" \
  --workload-identity-pool="POOL_ID" \
  --attribute-condition="assertion.repository_owner == 'my-org'"

For an AWS-based provider, pin the account and role:

gcloud iam workload-identity-pools providers update-aws PROVIDER_ID \
  --location="global" \
  --workload-identity-pool="POOL_ID" \
  --account-id="123456789012" \
  --attribute-condition="assertion.arn.startsWith('arn:aws:sts::123456789012:assumed-role/my-deploy-role')"

Danger: Updating a provider's attribute condition takes effect immediately. If you scope it too tightly, in-flight CI jobs and running workloads will start failing their token exchanges right away. Test the condition against a sample assertion and schedule the change with your deploy pipeline in mind.

Step 4: Tighten the IAM binding too

The condition is your first filter, but the principalSet in your service account IAM binding is your second. Make sure it is scoped to the same attribute, not a wildcard.

gcloud iam service-accounts add-iam-policy-binding deploy-sa@PROJECT_ID.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/attribute.repository/my-org/my-repo"

If you find an existing binding referencing attribute.repository_owner or no attribute at all, remove it and replace it with the narrowest principal that still works.


Defining it as code

If you manage GCP with Terraform, set the condition directly in the provider resource so it can never drift back to empty.

resource "google_iam_workload_identity_pool_provider" "github" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.main.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-provider"

  attribute_mapping = {
    "google.subject"             = "assertion.sub"
    "attribute.repository"       = "assertion.repository"
    "attribute.repository_owner" = "assertion.repository_owner"
    "attribute.ref"              = "assertion.ref"
  }

  attribute_condition = "assertion.repository_owner == 'my-org' && assertion.ref == 'refs/heads/main'"

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

Tip: Make attribute_condition a required input in your reusable module rather than an optional variable with a default of empty. That way nobody can stand up a new provider without consciously deciding who it trusts.


How to prevent it from happening again

One-off fixes do not scale. Bake the requirement into the tooling that creates these resources.

Policy as code with OPA / Conftest

Add a check to your Terraform plan validation that fails any provider without a non-empty condition.

package main

deny[msg] {
  resource := input.resource.google_iam_workload_identity_pool_provider[name]
  not resource.attribute_condition
  msg := sprintf("WIF provider '%s' must define attribute_condition", [name])
}

deny[msg] {
  resource := input.resource.google_iam_workload_identity_pool_provider[name]
  resource.attribute_condition == ""
  msg := sprintf("WIF provider '%s' has empty attribute_condition", [name])
}

CI/CD gate

Run that policy in the same pipeline stage as your terraform plan, before any apply. A failed policy blocks the merge, so the unconditioned provider never reaches production in the first place.

Tip: Pair the policy gate with continuous scanning from Lensix so you catch providers created through the console or gcloud outside Terraform. CI gates only see what flows through CI. Out-of-band changes need runtime detection.


Best practices

  • Always set an attribute condition. Treat an empty condition as a misconfiguration, not a default. There is almost no legitimate reason to trust an entire issuer.
  • Scope to the narrowest identity that works. Prefer a specific repository and branch over an org-wide owner match. Widen only when you have a concrete need.
  • Layer the condition and the IAM binding. The condition decides who can authenticate, the principalSet decides who can impersonate which service account. Scope both.
  • Avoid repository_owner-only conditions for sensitive accounts. A single compromised low-trust repo in your org should not unlock production.
  • Map only the attributes you need. Every mapped attribute is a potential condition input, but unused mappings add noise. Map what you will actually enforce on.
  • Review providers regularly. Federation configs are easy to set up and forget. Audit them on the same cadence as service account keys.

Workload Identity Federation removes the key files, but it does not remove the need to decide who you trust. The attribute condition is where that decision lives. Leave it empty and you have just traded one secret-management problem for an authorization problem that is harder to see.

Run the iam_workloadidentitynocondition check across your projects, fix the flagged providers, and put a policy gate in front of new ones. The remediation takes minutes. The exposure it closes is the difference between a controlled CI integration and an open door.