GitHub Actions to Google Cloud: Replace Service Account JSON Keys with Workload Identity Federation
Replace exported service account keys in GitHub Actions with Workload Identity Federation: compare three supported auth options, set up a conditional binding, and validate with positive and fork-PR negative tests.
07 Jan 2026, 15:59 UTC

The decision: exchange OIDC tokens, don't store keys
An exported service account key pasted into a GitHub Actions secret never expires on its own. It sits in the repository's secret store, widens your leak-scan surface, and revoking it means generating a new key and updating every workflow that references the old one. The decision this guide covers: authenticate GitHub-hosted CI to Google Cloud with Workload Identity Federation (WIF), which exchanges a short-lived GitHub OIDC token for temporary Google credentials, and reserve exported JSON keys for runners you genuinely cannot federate.
Constraints assumed throughout: GitHub-hosted runners, one deploy service account per environment (staging and production kept separate), least-privilege IAM on those accounts, and one-time setup done with the gcloud CLI. If your organization policy restricts workload identity pools, confirm availability with your org admin before designing around federation.
Three supported options
| Option | What GitHub stores | Revoking access | Blast radius | Fits |
|---|---|---|---|---|
| Exported JSON key in a secret | The full private key | Delete key, rotate in every workflow | Anyone who ever read the secret, until rotated | Throwaway experiments |
| Direct WIF binding | Pool and provider resource names (not secrets) | Remove or tighten the IAM condition | Repos and refs matching the condition | Most single-repo pipelines |
| WIF plus impersonation | Provider name plus target deploy SA | Two bindings: federation and token-creator | Smallest: federation only mints tokens for an unprivileged identity | Production, multi-repo setups |
What federation costs you
The price is one-time setup: a workload identity pool, an OIDC provider for GitHub's issuer, an attribute mapping (the sub claim to google.subject, plus repository and ref claims), and an attribute condition pinning trust to specific repositories and refs. In exchange you drop the rotation calendar, make revocation a policy edit instead of a key ceremony, and get token exchanges visible in Cloud Audit Logs.
The failure modes are worth naming:
- A
workloadIdentityUsergrant without a repository condition lets any repository that can obtain a token from GitHub's issuer mint credentials as that service account. Granting at project level instead of on the service account is worse still. - Pull requests from forks carry the fork's repository claim, so a repository condition rejects them — but only if the condition is actually present and you test it.
- OIDC tokens and the resulting Google credentials are short-lived; very long CI jobs must finish within the credential lifetime or re-authenticate mid-job.
Setup: pool, provider, conditional binding
Run the following in Cloud Shell or on a workstation with gcloud, against a scratch project first. Creating pools and providers needs roles/iam.workloadIdentityPoolAdmin; changing a service account's IAM policy needs roles/iam.serviceAccountAdmin (project owner covers both).
export PROJECT_ID=my-project
export PROJECT_NUMBER=$(gcloud projects describe \"$PROJECT_ID\" --format='value(projectNumber)')
export POOL=github-pool
export REPO=my-org/my-repo
gcloud iam workload-identity-pools create \"$POOL\" \\
--location=global \\
--display-name=\"GitHub Actions pool\"
gcloud iam workload-identity-pools providers create-oidc github \\
--location=global \\
--workload-identity-pool=\"$POOL\" \\
--issuer-uri=\"https://token.actions.githubusercontent.com\" \\
--attribute-mapping=\"google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref\" \\
--attribute-condition=\"assertion.repository=='$REPO'\"Now grant the pool's matching principals permission to act as the deploy service account. Note that the principal set uses the project number, not the project ID — a common mistake:
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/attribute.repository/$REPO\"For a production deploy account, tighten trust to the deploy branch by extending the provider condition, or create a second provider scoped to production:
--attribute-condition=\"assertion.repository=='$REPO' && assertion.ref=='refs/heads/main'\"Workflow changes
In the workflow, the id-token: write permission is what makes GitHub issue the OIDC token; without it the authentication step fails. Pin the current major version of the auth action rather than copying the placeholder below:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/auth@vX # pin the current major version
with:
workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github
service_account: deploy-sa@PROJECT_ID.iam.gserviceaccount.com
- run: gcloud projects describe PROJECT_IDThe action defaults to the provider's full resource name as the token audience, which matches the provider's default audience behavior. If you customize audiences on one side, customize both — a mismatch fails closed rather than silently.
Validation: positive, negative, and key hygiene
- Positive test: run the workflow on a push to the mapped repository. Expect
gcloud projects describeto print the project resource. - Negative test: open a pull request from a fork. Expect the authentication step to fail because the token's repository claim does not match the condition. If it succeeds, your condition is wrong — fix it before cutover.
- Key check: run
gcloud iam service-accounts keys list --iam-account=deploy-sa@PROJECT_ID.iam.gserviceaccount.com. Expect only Google-managed entries; any user-managed key should be deleted after cutover. - Config review:
gcloud iam workload-identity-pools providers describe github --location=global --workload-identity-pool=github-poolshows the mapping and condition as stored, andgcloud iam service-accounts get-iam-policyconfirms the principal-set binding.
Cutover and rollback
The migration is additive until the final step. Keep the JSON key secret in place while validating; if federation misbehaves, the workflow can fall back to the key path. Once both tests pass, delete the user-managed key (gcloud iam service-accounts keys delete KEY_ID) and remove the repository secret. After that point, rollback means minting a new key — exactly what you were eliminating — so treat the scratch-project validation as the safety net. Also note that deleting the pool or provider breaks CI until they are recreated.
Review notes and limits
- Attribute mapping and condition syntax have evolved over time. Verify the claim names (
repository,ref) and the condition expression against current Google Cloud documentation before shipping; this draft is flagged for that check. - For the impersonation variant, bind
workloadIdentityUserfor the pool on an unprivileged CI identity, then grantroles/iam.serviceAccountTokenCreatoron the deploy account to that identity. Check the auth action's current documentation for its chained-impersonation inputs rather than assuming input names. - Organization policies can restrict pool creation or external OIDC issuers; confirm in the target project before standardizing on this pattern.
- Multi-hour jobs may outlive the credential and need a re-authentication step.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.