Skip to content

Google Cloud OIDC with GitHub Actions

Lesson 4 of 10Advanced3 min readGitHub Actions & CI/CD · Actions SecurityVerified: google-github-actions/auth v3, Workload Identity Federation, August 2026

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.

PieceJob
Workload identity poolA container for external identities
ProviderTrusts one issuer — here, GitHub — and defines mapping and conditions
Attribute mappingTurns OIDC claims into Google attributes
Attribute conditionDecides which tokens are accepted at all
Service accountThe identity whose permissions are used (when impersonating)
IAM bindingSays which external identities may impersonate it
Terminal window
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.

The condition decides which tokens are accepted. The IAM binding decides which of those may impersonate a given service account:

Terminal window
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/production

Two independent controls, and defence in depth: the provider condition narrows by repository, the binding narrows by environment. Both must pass.

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 list

Direct 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:

Terminal window
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/repo

One 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.

SymptomUsual cause
Unable to acquire impersonated credentialsThe IAM binding does not match — check PROJECT_NUMBER and the attribute
The given credential is rejected by the attribute conditionThe condition is false for this token
Permission 'iam.serviceAccounts.getAccessToken' deniedMissing roles/iam.workloadIdentityUser binding
Unable to get ACTIONS_ID_TOKEN_REQUEST_URLid-token: write missing from permissions
Auth succeeds, the API call failsFederation is fine; the service account lacks the role
iam.serviceaccounts.actAs denied on deployDeploying 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.

  1. Create a pool and a provider with an attribute condition naming your repository.

  2. Confirm the condition is non-empty using the describe command above.

  3. Create a service account with roles/storage.objectViewer on one bucket, and bind roles/iam.workloadIdentityUser for your repository’s attribute.

  4. Authenticate from a workflow and run gcloud auth list. Confirm the impersonated account appears and that no key file exists.

  5. Tighten the condition to require assertion.ref == 'refs/heads/main' and run from a branch. Confirm the rejection names the attribute condition.

  6. Deliberately use PROJECT_ID instead of PROJECT_NUMBER in the binding and observe that it fails silently rather than erroring at creation time.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Get the production security checklists and Actions hardening templates from the Professional Toolkit.