Google’s mechanism has the most moving parts of the three clouds — a pool, a provider, attribute mappings, an attribute condition, and usually a service account to impersonate. Each piece has a distinct job, and the errors are much easier to read once you know which piece each belongs to.
The pieces
Section titled “The pieces”| Piece | Job |
|---|---|
| Workload identity pool | A container for external identities |
| Provider | Trusts one issuer — here, GitHub — and defines mapping and conditions |
| Attribute mapping | Turns OIDC claims into Google attributes |
| Attribute condition | Decides which tokens are accepted at all |
| Service account | The identity whose permissions are used (when impersonating) |
| IAM binding | Says which external identities may impersonate it |
Create the pool and provider
Section titled “Create the pool and provider”gcloud iam workload-identity-pools create github \ --location=global \ --display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc repo \ --location=global \ --workload-identity-pool=github \ --issuer-uri="https://token.actions.githubusercontent.com" \ --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref,attribute.environment=assertion.environment" \ --attribute-condition="assertion.repository == 'OWNER/REPO'"--attribute-mapping makes claims usable. google.subject is required; the rest become
attribute.* values you can write conditions and IAM bindings against. Map only what you will use —
an unmapped claim simply is not available later, and adding one means editing the provider.
--attribute-condition is the gate. It is a CEL expression evaluated against the token; if it is
false, the exchange fails before anything else is considered.
Conditions worth knowing, from narrowest:
assertion.repository == 'OWNER/REPO' && assertion.ref == 'refs/heads/main'assertion.repository == 'OWNER/REPO' && assertion.environment == 'production'assertion.repository == 'OWNER/REPO'assertion.repository_owner == 'OWNER'The last one trusts every repository your organisation owns, including one created tomorrow by someone who should not have production access. Use it only with a correspondingly narrow IAM binding.
Bind the service account
Section titled “Bind the service account”The condition decides which tokens are accepted. The IAM binding decides which of those may impersonate a given service account:
gcloud iam service-accounts add-iam-policy-binding \ deployer@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.repository/OWNER/REPO"The principalSet:// member selects by a mapped attribute. To restrict to one environment, bind on
that attribute instead:
principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.environment/productionTwo independent controls, and defence in depth: the provider condition narrows by repository, the binding narrows by environment. Both must pass.
Authenticating
Section titled “Authenticating”permissions: contents: read id-token: write
jobs: deploy: runs-on: ubuntu-latest environment: production steps: - uses: google-github-actions/auth@v3 with: workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/providers/repo service_account: deployer@PROJECT_ID.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v3 - run: gcloud auth listDirect federation without a service account
Section titled “Direct federation without a service account”Newer configurations can skip impersonation and grant IAM roles straight to the federated principal:
gcloud projects add-iam-policy-binding PROJECT_ID \ --role=roles/run.developer \ --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.repository/OWNER/REPO"- uses: google-github-actions/auth@v3 with: workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/providers/repoOne fewer identity to manage, and no service account whose keys could be created later. The trade-off is that not every Google Cloud service accepts a federated principal directly, and some operations still require acting as a service account. Impersonation remains the more broadly compatible option.
Diagnosing failures
Section titled “Diagnosing failures”| Symptom | Usual cause |
|---|---|
Unable to acquire impersonated credentials | The IAM binding does not match — check PROJECT_NUMBER and the attribute |
The given credential is rejected by the attribute condition | The condition is false for this token |
Permission 'iam.serviceAccounts.getAccessToken' denied | Missing roles/iam.workloadIdentityUser binding |
Unable to get ACTIONS_ID_TOKEN_REQUEST_URL | id-token: write missing from permissions |
| Auth succeeds, the API call fails | Federation is fine; the service account lacks the role |
iam.serviceaccounts.actAs denied on deploy | Deploying a service that runs as another identity needs roles/iam.serviceAccountUser on that runtime identity |
That last one catches nearly everyone deploying Cloud Run for the first time, and it is not a federation problem at all.
As with the other clouds, print the real claims using the inspection step in
OIDC and compare against the condition. A condition referencing
assertion.environment is false for a job that declared no environment — the claim is absent, not
empty.
Exercise
Section titled “Exercise”-
Create a pool and a provider with an attribute condition naming your repository.
-
Confirm the condition is non-empty using the
describecommand above. -
Create a service account with
roles/storage.objectVieweron one bucket, and bindroles/iam.workloadIdentityUserfor your repository’s attribute. -
Authenticate from a workflow and run
gcloud auth list. Confirm the impersonated account appears and that no key file exists. -
Tighten the condition to require
assertion.ref == 'refs/heads/main'and run from a branch. Confirm the rejection names the attribute condition. -
Deliberately use
PROJECT_IDinstead ofPROJECT_NUMBERin the binding and observe that it fails silently rather than erroring at creation time.