Workload Identity Federation (WIF) is the correct modern approach to GCP authentication for external workloads. It eliminates the need to create and distribute service account key files by allowing external identities — GitHub Actions workflows, AWS EC2 instance profiles, Azure managed identities, on-premises Kubernetes workloads — to exchange their native credentials for short-lived GCP access tokens. No static secrets to rotate, no key files to accidentally commit.
The problem is in the trust rules. When those rules are too broad, WIF creates a privilege escalation path that is substantially worse than the service account keys it replaced: an attacker who compromises any identity trusted by an overly permissive pool gains access to all GCP permissions bound to the attached service account — and unlike key files, there may be no secret to rotate because the trust rule is configuration, not a credential.
How WIF Trust Chains Work
A Workload Identity Pool is a GCP resource that defines a set of external identity providers (IdPs) trusted to exchange tokens for GCP access. Each pool can have multiple provider configurations. A provider maps to one external IdP — a GitHub Actions OIDC endpoint, an AWS STS endpoint, a corporate Kubernetes cluster.
The attack surface lives in attribute conditions on provider configurations and service account bindings. When a workload requests a GCP token via WIF, the federation service evaluates:
- Does the presented token come from a trusted IdP (the provider’s issuer URL)?
- Does the token’s attributes satisfy the attribute condition on the provider?
- Is there a service account binding that grants this workload identity
roles/iam.workloadIdentityUseron the target service account?
If the attribute condition is overly broad — or absent — any identity that can obtain a token from the trusted IdP can satisfy step 2.
The GitHub Actions Over-Trust Pattern
GitHub’s OIDC endpoint (token.actions.githubusercontent.com) is a multi-tenant IdP: it issues tokens to any GitHub Actions workflow on the entire platform. If a WIF provider trusts this endpoint without an attribute condition scoped to a specific repository, owner, or workflow, the following is possible:
- Attacker creates a GitHub account and a repository.
- Attacker writes a GitHub Actions workflow that requests a GitHub OIDC token and exchanges it for a GCP token using the target organisation’s WIF pool.
- If the pool provider has no attribute condition (or a condition that does not check
google.subject,attribute.repository, orattribute.repository_owner), the federation succeeds. - Attacker now holds a short-lived GCP access token with the permissions of whatever service account is bound to the pool identity.
This attack requires knowledge of the target’s WIF pool name and project ID — both of which may be visible in public CI/CD configuration, Terraform files, or GitHub Actions workflow files checked into public repositories.
Auditing Your WIF Configuration
List all Workload Identity Pools and Providers
# List pools in your project
gcloud iam workload-identity-pools list \
--project=YOUR_PROJECT_ID \
--location=global \
--format="table(name, state, displayName)"
# For each pool, list providers
gcloud iam workload-identity-pools providers list \
--workload-identity-pool=POOL_ID \
--project=YOUR_PROJECT_ID \
--location=global \
--format="json"
Inspect Provider Attribute Conditions
gcloud iam workload-identity-pools providers describe PROVIDER_ID \
--workload-identity-pool=POOL_ID \
--project=YOUR_PROJECT_ID \
--location=global \
--format="json(attributeCondition, attributeMapping, oidc)"
Look for providers where attributeCondition is null or empty. For GitHub OIDC providers specifically, a missing condition means any GitHub workflow can exchange a token against this pool.
Find Service Account Bindings
# Find which service accounts grant workloadIdentityUser to your pool
gcloud iam service-accounts get-iam-policy SA_EMAIL \
--project=YOUR_PROJECT_ID \
--format=json | jq '.bindings[] | select(.role == "roles/iam.workloadIdentityUser")'
This reveals the blast radius: which service accounts are reachable from an overly broad pool, and what GCP permissions those accounts hold.
Hardening: Attribute Conditions That Actually Constrain Access
GitHub Actions — Minimum Viable Restriction
# Require tokens to come from a specific repository AND branch
gcloud iam workload-identity-pools providers update-oidc PROVIDER_ID \
--workload-identity-pool=POOL_ID \
--project=YOUR_PROJECT_ID \
--location=global \
--attribute-condition="assertion.repository == 'your-org/your-repo' && assertion.ref == 'refs/heads/main'"
For production deployments, also require an Actions environment:
--attribute-condition="assertion.repository == 'your-org/your-repo' && assertion.environment == 'production'"
Attribute Mapping for Fine-Grained Conditions
Define a mapping so GCP sees the GitHub claim fields you want to filter on:
gcloud iam workload-identity-pools providers update-oidc PROVIDER_ID \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref,attribute.environment=assertion.environment" \
--workload-identity-pool=POOL_ID \
--project=YOUR_PROJECT_ID \
--location=global
Then reference these in your condition:
attribute.repository_owner == 'your-org' && attribute.repository == 'your-org/your-repo'
Multi-Provider Pool Risk
If your pool has providers for multiple external IdPs (GitHub, AWS, and a Kubernetes cluster, for example), a service account bound to the pool’s member principal (principalSet://...) is accessible from all of them. Compromise of any one federated identity provider reaches the same GCP service account.
The fix: separate pools per workload type, with separate service account bindings scoped to each pool’s specific principal set. Do not bind a high-privilege service account to a pool that contains a broadly trusted provider.
Common Misconfigurations Found in the Wild
| Misconfiguration | Impact | Detection Method |
|---|---|---|
GitHub OIDC provider with no attributeCondition | Any GitHub Actions workflow can impersonate the bound SA | providers describe — null attributeCondition |
| Multi-tenant provider trusted for entire org rather than specific repo | Any repo in the org can escalate | Check attribute.repository_owner only vs. full repo path |
High-privilege SA (e.g. roles/owner) bound to a broadly trusted pool | Full project compromise from external identity | IAM policy audit on SA |
| Pool created for testing, not deleted, still has SA bindings | Orphan attack surface | List all pools including deleted state |
google.subject mapped but no attribute condition checking it | Subject condition bypassed if IdP allows subject manipulation | Review mapping + condition for logical gaps |
Monitoring and Alerting
GCP logs WIF token exchanges in the IAM service under the activity log. Alert on unexpected patterns:
# Find WIF token issuance events in Cloud Logging
gcloud logging read \
'protoPayload.methodName="google.iam.credentials.v1.IAMCredentials.GenerateIdToken"
OR protoPayload.methodName="ExchangeToken"' \
--project=YOUR_PROJECT_ID \
--format="json" \
--freshness=1h
Specific conditions worth alerting on:
- WIF token exchanges from source IPs in unexpected geographies (GitHub Actions runners have documented IP ranges — exchanges outside these indicate a non-GitHub source using your pool)
- Token exchanges for service accounts with elevated privileges (
roles/owner,roles/editor, custom roles with sensitive permissions) from any workload pool - Token exchanges from Entra ID or AWS identities where the corresponding workload does not have a known CI/CD job running
WIF is the right architecture for keyless GCP access. The security model holds when attribute conditions are specific. The failure mode is treating a multi-tenant IdP like GitHub as a single trusted entity rather than a platform containing millions of untrusted repositories alongside your own.