A leaked JSON key is not a hypothetical. It is one of the most common ways CI/CD pipelines get compromised, because a service account key never expires on its own, rarely gets rotated, and grants whoever holds it the same access as the pipeline itself. Google Cloud has spent the past two years pushing customers away from that model. As of 2026, new Google Cloud organizations have service account key creation disabled by default through the iam.disableServiceAccountKeyCreation organization policy constraint, and any attempt to create one fails with a blunt FAILED_PRECONDITION error.
The replacement is workload identity federation, a mechanism that lets GitHub Actions, GitLab CI, Kubernetes pods, and even other clouds exchange a short-lived token for GCP access without ever storing a credential file. This tutorial walks through the full setup in 13 concrete steps, from the first pool to a production Cloud Run deployment that never touches a key. It fits into the broader shift covered in our cloud computing strategy hub, where identity, cost, and automation keep colliding in 2026 infrastructure decisions.
Expect about 90 minutes end to end if you follow along on a real GCP project. You will need owner or IAM admin access on that project, plus a GitHub repository you control. By the end, your deploy pipeline authenticates with a token that lives for roughly one hour and cannot be replayed once it expires.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Workload Identity Federation Actually Replaces
Before workload identity federation, the standard pattern for CI/CD access to GCP looked like this: create a service account, generate a JSON key, paste that key into a CI secret store, and hope nobody ever exports the build logs. That key had no expiration date. If a contractor left the company, if a CI provider suffered a breach, or if someone accidentally committed the file to a public repo, the key kept working until a human noticed and revoked it.
Workload identity federation flips the trust model. Instead of GCP trusting a static secret, it trusts an external identity provider directly. GitHub’s Actions runner, for example, already issues a signed OpenID Connect (OIDC) token for every workflow run, scoped to that specific repository and branch. You configure GCP to trust GitHub’s OIDC issuer, map specific token claims to GCP identity attributes, and grant a narrow IAM role to whichever identity matches. When the workflow runs, GitHub hands GCP that token, GCP verifies the signature against GitHub’s public keys, and it mints a short-lived access token in return. No file. No rotation schedule. No secret to leak.
The same mechanism now covers AWS IAM roles calling into GCP, Kubernetes service accounts on GKE, and workloads running on Azure. Cloud vendors converged on the pattern for a reason: it removes the single biggest source of long-lived, unmonitored credentials in modern infrastructure. If you already run cross-cloud IAM, the concepts overlap heavily with what we cover in our guide to setting up AWS IAM Identity Center, since both systems trade static secrets for short-lived, federated trust.
The push is not purely defensive. Teams that finish this kind of migration usually report a side benefit nobody planned for going in: onboarding new pipelines gets faster, not slower. Provisioning a key, storing it as an encrypted CI secret, documenting where it lives, and remembering to revoke it when a project shuts down all disappear. What replaces that process is a short gcloud script that runs once per repository and never needs a rotation reminder on anyone’s calendar.
One nuance trips up almost everyone on their first attempt. Workload identity federation on its own only tells GCP who the caller claims to be. It does not grant permissions. You still need to bind that federated identity to a service account, or grant roles directly to the pool, and attach IAM roles the same way you would for any other principal. Skip that step and the token exchange succeeds while every API call after it returns a 403.
Workforce Identity Federation vs Workload Identity Federation
Google Cloud actually ships two related but distinct products, and mixing them up wastes an afternoon of documentation searches. Workforce identity federation is for humans, letting employees sign into the Google Cloud console or gcloud CLI using an existing corporate identity provider like Okta or Microsoft Entra ID, without Google ever managing their password. Workload identity federation, the subject of this guide, is for machines: CI runners, containers, and services that need to call GCP APIs on their own, with no human typing a password at all.
The underlying mechanics overlap heavily since both trade a static credential for a federated trust relationship. But the pools, providers, and IAM surfaces are separate resources, and console documentation sometimes uses the shortened “identity federation” phrase for either one. If a tutorial mentions signing in through a browser redirect, it almost certainly means the workforce product, not the one this guide covers.
Multi-cloud teams run into a related but separate scenario: an AWS Lambda function or an Azure Function that needs to call a GCP API directly, with no GitHub Actions runner in the picture at all. The same pool-and-provider model applies, just with an AWS or Azure provider type instead of an OIDC issuer, using the calling service’s native IAM role or managed identity as the trusted source. The configuration commands differ slightly, but the pool, the attribute condition, and the IAM binding all follow the pattern this tutorial builds step by step.
Prerequisites: Tools, Versions, and Access You Need
Get these lined up before you touch the console. Version mismatches account for a good chunk of the errors people hit halfway through a workload identity federation setup.
| Requirement | Minimum / Current Version | Notes |
|---|---|---|
| gcloud CLI | 584.0.0 (Sept. 2026) | Older builds lack the newer OIDC mapping flags |
| GCP IAM permissions | roles/iam.workloadIdentityPoolAdmin | Project-level, or Owner during initial setup |
| Terraform google provider | 8.2.0 | Only needed if you manage the pool as code |
| google-github-actions/auth | v3 | GitHub Marketplace action for the token exchange |
| google-github-actions/deploy-cloudrun | v3 | Used in the end-to-end example later in this guide |
| GitHub repository access | Admin | Needed to add the workflow file and confirm the OIDC issuer |
| kubectl (for the GKE section) | 1.34 or newer | Matches current GKE Autopilot and Standard clusters |
You also need a GCP project with billing enabled and the following APIs turned on: IAM Service Account Credentials API, IAM API, and Cloud Resource Manager API. Run this once before Step 1:
gcloud services enable \
iamcredentials.googleapis.com \
iam.googleapis.com \
cloudresourcemanager.googleapis.com \
--project=YOUR_PROJECT_ID
If your organization already enforces iam.disableServiceAccountKeyCreation, you cannot fall back to a JSON key even temporarily, which is exactly the point. Plan on workload identity federation as the only path forward, not a nice-to-have.
How Workload Identity Federation Works, Step by Step
How the OIDC Token Exchange Actually Works
Three objects make up a working setup: a workload identity pool, a provider inside that pool, and an IAM binding that connects the two to a service account or resource. Here is the sequence for a single GitHub Actions run.
- GitHub’s Actions runner requests a short-lived OIDC token from GitHub’s own token service, scoped to the current repository, branch, and workflow.
- The workflow calls the
google-github-actions/authaction, which sends that token to Google’s Security Token Service (STS) endpoint. - GCP checks the token’s signature against GitHub’s published JSON Web Key Set, confirms the issuer matches your configured provider, and evaluates your attribute condition.
- If everything matches, STS returns a federated GCP token representing an “external” identity inside your pool.
- That federated identity impersonates a real service account, or with direct resource access calls APIs on its own, and GCP issues a normal access token good for about one hour.
The part people underestimate is the attribute condition. Without one, any repository under any GitHub organization that happens to know your pool ID could request a token. Google’s own guidance treats an unrestricted provider as a configuration bug, not an edge case. A correctly scoped condition looks something like assertion.repository_owner == 'your-org' && assertion.repository == 'your-org/your-repo', checked at the provider level so a misconfigured downstream binding cannot widen the blast radius.
Google documents the full trust model, including STS internals and supported identity providers, in its workload identity federation reference. Worth reading once even if you follow this tutorial step by step, because the terminology (pool, provider, principal, principalSet) shows up constantly in error messages.
GitHub is the example this tutorial builds around, but the same exchange works with any OIDC-compliant issuer. GitLab CI, Bitbucket Pipelines, and CircleCI all issue their own signed tokens per job, and each maps to a GCP provider the same way, just pointed at a different issuer URL and a different set of claim names. The commands change by a few flags. The trust model does not. GitHub documents its side of the exchange, including the exact claims available in the token, in its guide to configuring OpenID Connect for Google Cloud Platform.
The 13 Steps at a Glance
- Enable the required GCP APIs
- Create the workload identity pool
- Create the OIDC provider inside the pool
- Write and test the attribute condition
- Map OIDC claims to GCP attributes
- Create the dedicated deploy service account
- Grant least-privilege IAM roles on that service account
- Bind the pool’s principal to the service account via workloadIdentityUser
- Retrieve the provider’s full resource name
- Add the GitHub Actions workflow using google-github-actions/auth
- Verify the token exchange with a dry-run job
- Extend the pattern to GKE workloads with Workload Identity
- Decommission any leftover service account keys
The next sections work through each group of steps with the actual commands. Copy them in order and substitute your own project ID, repository name, and region.
Steps 1-4: Building the Workload Identity Pool and Provider
With the APIs already enabled from the prerequisites section, create the pool first. Think of a pool as a container that groups related external identities, so most teams create one pool per environment or per major CI system rather than one per repository. A pool by itself does nothing. It only becomes useful once a provider inside it starts accepting tokens from a real issuer, which is why steps 2 and 3 always run back to back.
Naming matters more than it looks. Once a pool ID is set, it cannot change, and it becomes part of every principal string you write later. Pick something that survives a reorg, like github-actions-pool rather than a team name that might not exist in a year.
gcloud iam workload-identity-pools create "github-actions-pool" \
--project="YOUR_PROJECT_ID" \
--location="global" \
--display-name="GitHub Actions Pool"
Next, create the OIDC provider inside that pool, pointing at GitHub’s issuer and mapping the claims you will need later.
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--project="YOUR_PROJECT_ID" \
--location="global" \
--workload-identity-pool="github-actions-pool" \
--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-condition="assertion.repository_owner == 'YOUR_GITHUB_ORG'"
That last flag, --attribute-condition, is step 4 and the one Google’s own security team calls out repeatedly in its guidance on service account best practices. Set it too loosely and you have effectively handed trust to every repository your organization has ever created. Test it against a single repo first, then widen scope deliberately once you understand the blast radius.
Expect output resembling this after the provider creates successfully:
Created workload identity pool provider [github-provider].
name: projects/123456789012/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
state: ACTIVE
That numeric project number, not your project ID, is what shows up in the resource name you will paste into the GitHub Actions workflow later. Save it now, since Step 9 depends on it.
Steps 5-7: Attribute Mapping and Least-Privilege IAM Roles
Attribute mapping decides which pieces of the GitHub OIDC token become usable GCP attributes. The table below shows a typical mapping and what it lets you condition on later.
| OIDC Claim | Mapped GCP Attribute | Used For |
|---|---|---|
| sub | google.subject | Unique identity string, logged on every API call |
| repository | attribute.repository | Restricting access to org/repo-name exactly |
| repository_owner | attribute.repository_owner | Restricting access to a GitHub org |
| ref | attribute.ref | Limiting deploys to a specific branch, e.g. refs/heads/main |
| workflow | attribute.workflow | Restricting to a named workflow file |
With mapping in place, create a dedicated service account for the deploy pipeline. Do not reuse an existing broad-permission account. A narrow, single-purpose identity limits what an attacker gains even if the pipeline itself is later compromised.
gcloud iam service-accounts create "gha-deployer" \
--project="YOUR_PROJECT_ID" \
--display-name="GitHub Actions Deployer"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.developer"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"
Grant only what the pipeline actually does. A workflow that deploys to Cloud Run needs roles/run.developer, not roles/editor. If your pipeline also pushes container images, add roles/artifactregistry.writer scoped to the specific repository rather than the whole project.
Resist the urge to grant Owner or Editor “temporarily” while debugging. That temporary grant tends to outlive the debugging session by months, and it defeats the entire purpose of moving off static keys in the first place. If a deploy step fails with a permission error, add the single missing role, test again, and move on. Broad roles hide real bugs instead of fixing them, since almost any action will succeed regardless of whether the pipeline actually needs that access.
Steps 8-9: Binding the Pool to the Service Account
This is where most people either lock themselves out or leave the door too wide open. The binding connects a principal, or principal set, from your pool to the service account, using the roles/iam.workloadIdentityUser role.
principalSet vs principal: Choosing the Right Binding Scope
GCP gives you two ways to reference a federated identity in an IAM binding. A single principal:// reference matches one exact identity, useful when a single, well-known workflow needs access and nothing else ever should. A principalSet:// reference matches any identity satisfying an attribute pattern, which is what almost every real setup uses since it scales to new branches and workflow files without touching IAM again. The example in this guide uses a principalSet scoped to attribute.repository, which is the right default for most teams. Reach for a single principal binding only when you want to lock access to one specific workflow run pattern, such as a scheduled release job that should never be triggered by a normal push.
gcloud iam service-accounts add-iam-policy-binding \
"gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--project="YOUR_PROJECT_ID" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/YOUR_GITHUB_ORG/YOUR_REPO"
Notice the binding scopes to attribute.repository, meaning only workflows running from that exact repository can impersonate this service account. Scoping to attribute.repository_owner instead would let any repo in the organization impersonate it, which is rarely what you want for a production deploy account.
Retrieve the full provider resource name now, since the GitHub Actions workflow needs the exact string.
gcloud iam workload-identity-pools providers describe "github-provider" \
--project="YOUR_PROJECT_ID" \
--location="global" \
--workload-identity-pool="github-actions-pool" \
--format="value(name)"
Steps 10-11: Wiring Up GitHub Actions for Keyless Deploys
With the pool, provider, and binding in place, the workflow file itself is short. The critical part is the permissions block, which grants the job an id-token so GitHub can even issue the OIDC token in the first place. Forget that line and the auth action fails before it reaches GCP.
name: Deploy to Cloud Run
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v3
with:
project_id: YOUR_PROJECT_ID
workload_identity_provider: projects/123456789012/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
service_account: gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com
- uses: google-github-actions/deploy-cloudrun@v3
with:
service: hello-wif
region: us-central1
image: us-central1-docker.pkg.dev/YOUR_PROJECT_ID/app/hello-wif:${{ github.sha }}
Push that file and watch the Actions log. Step 11, verification, comes down to checking two things: the auth step logs a token exchange without errors, and the deploy step succeeds without ever referencing a secret named anything like GCP_SA_KEY. If your repository still has that secret defined from an earlier setup, delete it once the new workflow runs clean, and treat every day it remains as extra exposure. The google-github-actions/auth repository documents every input the action accepts, including options this tutorial does not cover, like generating an OAuth 2.0 access token directly instead of a credentials file.
A successful auth step logs output close to this:
Created credentials file at "/home/runner/gha-creds-abc123.json"
::add-mask::***
google-github-actions/auth: Successfully authenticated as [gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com]
That credentials file is temporary, scoped to the job’s lifetime, and automatically cleaned up after the run. It is not a secret you manage. That distinction, ephemeral file versus stored key, is the entire point of this approach.
Long-running jobs need one extra consideration. Since the token this exchange produces lasts about an hour, a build-and-deploy job that runs 90 minutes could hit an expired token partway through. The google-github-actions/auth action supports a token_format input and can be re-invoked mid-workflow if a later step needs a fresh token. For most CI/CD pipelines that finish in under 20 minutes this never comes up, but data pipelines or long test suites that call GCP APIs throughout the run should plan for a refresh step rather than assuming the original token survives to the end.
Steps 12-13: Extending Workload Identity Federation to GKE Pods
The same trust model applies inside a Kubernetes cluster, where it is usually called GKE Workload Identity. Instead of federating an external OIDC provider, GKE lets you bind a Kubernetes service account directly to a GCP service account, and the cluster’s metadata server issues short-lived tokens to pods automatically. If you have not stood up the cluster yet, our Google Kubernetes Engine setup guide covers that groundwork before you get here.
gcloud container clusters update YOUR_CLUSTER \
--region=us-central1 \
--workload-pool=YOUR_PROJECT_ID.svc.id.goog
kubectl create serviceaccount k8s-deployer --namespace=default
gcloud iam service-accounts add-iam-policy-binding \
"gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:YOUR_PROJECT_ID.svc.id.goog[default/k8s-deployer]"
kubectl annotate serviceaccount k8s-deployer \
--namespace=default \
iam.gke.io/gcp-service-account=gha-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com
Any pod that runs under the k8s-deployer Kubernetes service account now gets a real GCP identity token from the node’s metadata server, with no JSON key ever mounted as a volume. Google’s own GKE Workload Identity documentation covers the Autopilot-specific defaults, which enable this automatically on new clusters.
Step 13 is the cleanup pass. Search every service account in the project for user-managed keys and delete anything that predates this migration.
for sa in $(gcloud iam service-accounts list --project=YOUR_PROJECT_ID --format="value(email)"); do
gcloud iam service-accounts keys list --iam-account="$sa" \
--filter="keyType=USER_MANAGED" --format="value(name)"
done
Anything listed there is a standing liability. Once you confirm the matching workload runs cleanly on the new setup, delete the key and move to the next service account.
Complete Working Example: A Zero-Key Cloud Run Pipeline
Tying every step together, here is what a finished setup looks like for a small team deploying a containerized API to Cloud Run on every push to main. Assume the project ID is acme-prod, the GitHub org is acme-corp, and the repo is acme-api.
- Pool:
github-actions-pool, created once per project. - Provider:
github-provider, attribute condition locked toassertion.repository == 'acme-corp/acme-api'. - Service account:
[email protected], holding onlyroles/run.developerandroles/artifactregistry.writeron the specific repository. - Binding: principalSet scoped to the exact repo path, granting
roles/iam.workloadIdentityUser. - Workflow: builds the container, pushes to Artifact Registry, deploys via deploy-cloudrun@v3.
The full workflow, combining the build and deploy steps into one file that never references a stored credential:
name: Build and Deploy
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
env:
PROJECT_ID: acme-prod
REGION: us-central1
REPO: acme-api
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v3
with:
project_id: ${{ env.PROJECT_ID }}
workload_identity_provider: projects/123456789012/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
service_account: [email protected]
- uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet
- name: Build and push image
run: |
docker build -t ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/api:${{ github.sha }} .
docker push ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/api:${{ github.sha }}
- uses: google-github-actions/deploy-cloudrun@v3
with:
service: acme-api
region: ${{ env.REGION }}
image: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/api:${{ github.sha }}
If you have not already stood up the Cloud Run service itself, walk through our Google Cloud Run setup guide first, then come back and swap the credential step for this workload identity federation flow. Teams that make this switch typically report the migration itself takes an afternoon per pipeline, with the bulk of the time spent scoping IAM roles down rather than configuring the pool.
Verify the deployment landed correctly before calling the migration done. A quick check against the Cloud Run service confirms the image tag matches the commit that triggered the workflow.
gcloud run services describe acme-api \
--region=us-central1 \
--format="value(status.latestReadyRevisionName,spec.template.spec.containers[0].image)"
If that output shows the expected image digest and the service responds on its URL, the pipeline is complete: build, push, and deploy, all authenticated through a token that existed for less time than it took to read this paragraph.
Common Pitfalls When Setting Up Workload Identity Federation
These five mistakes account for most of the failed first attempts.
- Skipping the attribute condition entirely. A provider with no condition trusts every repository under the issuer, not just yours.
- Binding at the repository_owner level when repository level was intended, silently widening trust to every repo in the org.
- Forgetting the
id-token: writepermission in the workflow, which causes the token request to fail before it ever reaches GCP. - Granting broad roles like Editor “just to get it working,” then never tightening scope afterward.
- Mixing up the project ID and the numeric project number in the provider resource path. The resource name always uses the number, not the ID.
A sixth, less technical pitfall shows up on teams migrating from an older setup: leaving the old JSON key active “just in case” during the transition. If that key sits in a build log or a forked repository somewhere, it stays a live risk until it is revoked, regardless of how well the new setup works. Our guide on stopping leaked secrets in git with Gitleaks is worth running across your repo history before you consider the migration finished.
A seventh pitfall specifically hits fork-based workflows. GitHub’s OIDC token for a pull request coming from a fork carries different claim values than one from a branch in the main repository, and by default GitHub does not even grant id-token: write permission to workflows triggered by forked pull requests, for good reason. If your CI needs to run against forks, do not attempt to widen the attribute condition to compensate. Instead, split the workflow so the deploy job only runs on pushes to trusted branches, and let fork-triggered jobs stay read-only.
Troubleshooting Workload Identity Federation Errors
Eight errors show up constantly in support forums and GitHub issue trackers. Here is what each one actually means, and the table below maps each one to a fix you can apply in a few minutes.
| Error | Likely Cause | Fix |
|---|---|---|
| FAILED_PRECONDITION on key creation | Org policy disables service account keys | Expected behavior. Use workload identity federation instead. |
| PERMISSION_DENIED: does not have permission to access | Binding exists but IAM role missing | Grant the actual role (run.developer, storage.admin, etc.) separately from workloadIdentityUser |
| UNAUTHENTICATED: Invalid JWT | Clock skew or wrong issuer URI | Confirm issuer is exactly token.actions.githubusercontent.com |
| Token exchange failed: attribute condition evaluation error | Malformed condition syntax | Validate with the providers describe command first |
| id-token permission missing | Workflow lacks permissions block | Add id-token: write at job or workflow level |
| Unable to acquire impersonated credentials | Missing serviceAccountTokenCreator on the calling identity | Grant roles/iam.serviceAccountTokenCreator alongside workloadIdentityUser |
| NOT_FOUND: Requested entity was not found | Wrong project number in resource path | Re-run the describe command and copy the exact name field |
| GKE pod cannot reach metadata server for token | Workload Identity not enabled on the node pool | Confirm GKE_METADATA is set on the node pool, not just the cluster |
Most of these resolve in under ten minutes once you know which layer to check: the provider condition, the IAM binding, or the workflow permissions block. Almost none of them are actual GCP outages, despite how the error text reads.
Two more show up less often but waste more time when they do. A workflow that runs fine locally against a personal GCP project but fails in the shared CI environment usually means the pool or provider only exists in the personal project, not the one CI actually targets. Double-check the --project flag on every command from Step 2 onward, since it is easy to create the pool in the wrong project during testing and never notice until a teammate tries the same workflow. The other recurring issue is a provider that worked for weeks and then suddenly stops, often traced back to someone rotating or narrowing an org policy without checking which pipelines depend on the existing attribute condition. Keep a changelog of who touches pool and provider configuration, the same way you would for a production database schema.
Advanced Tips for Hardening and Scaling Your Setup
Once the basic pipeline works, a few refinements pay off at scale. First, create one pool per environment, such as dev, staging, and prod, rather than one shared pool, so a compromised staging pipeline cannot request tokens against production service accounts. Second, use principal-level bindings tied to branch refs for production deploys specifically, so only pushes to main, not arbitrary feature branches, can impersonate the production deploy account.
Third, if you manage infrastructure with Terraform, define the pool, provider, and bindings as code rather than one-off gcloud commands. The hashicorp/google provider at 8.2.0 supports the full resource set, including google_iam_workload_identity_pool and google_iam_workload_identity_pool_provider, which keeps the attribute condition under version control and code review instead of living only in someone’s shell history.
Fourth, if your organization already runs a secrets manager for database credentials or API keys unrelated to CI/CD, do not assume workload identity federation replaces it. It solves machine-to-GCP authentication specifically. You still need a plan for application secrets like third-party API keys, and our comparison of HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault is a reasonable starting point for that separate problem.
Fifth, monitor the pool itself. Cloud Audit Logs record every token exchange against a workload identity pool, including the source repository attribute. Set up a log-based alert for exchanges from unexpected repositories or refs, since that is the earliest signal something is misconfigured or, worse, being abused.
Sixth, tie the migration into your existing FinOps reporting rather than treating it as a pure security project. Credential incidents are expensive to clean up: rotating every downstream secret, auditing every API call made under a compromised identity, and the engineering hours lost to the response all show up somewhere in a budget, even if no line item says “key leak.” Framing this rollout as risk reduction with a dollar figure attached tends to get it prioritized faster than a security ticket alone, especially on teams where FinOps and platform engineering already share a roadmap.
Workload Identity Federation vs Service Account Keys
The practical difference comes down to token lifetime and blast radius. A service account key has no built-in expiration and grants standing access the moment it exists. A workload identity federation token lives for about an hour, gets minted fresh for every job run, and cannot be reused once a workflow finishes. If an attacker captures a build log mid-run, they get a token that is likely already expired by the time they can use it, rather than a key that works for months.
Operationally, keys also carry a maintenance cost that federation removes outright. Someone has to track which keys exist, when they were created, whether they are still in use, and rotate them on a schedule that rarely survives contact with a busy team. Workload identity federation has no rotation schedule because there is nothing stored to rotate. The setup cost is front-loaded, roughly the 90 minutes this tutorial takes, against an ongoing cost of effectively zero for key management afterward.
Migrating Existing Pipelines Off Service Account Keys
If you are retrofitting this approach onto pipelines that already use keys, do it one repository at a time rather than in one large cutover. Start with a low-risk pipeline, confirm the workflow runs clean for a few days, then move to the next. Keep the old key active but unused during that window, and only revoke it once the new path has proven itself in production, not just in a test branch.
Audit which keys actually get used before deleting anything. A key that has not authenticated in 90 days is a strong candidate for immediate deletion regardless of migration status, since an unused key is pure risk with no offsetting benefit. Google Cloud’s IAM console surfaces last-used timestamps for service account keys, which makes this audit a matter of minutes rather than guesswork.
Track migration progress with a simple checklist per repository: pool and provider created, service account scoped, IAM binding tested, workflow updated, old key deleted. Teams running dozens of repositories often build a small internal dashboard against the Cloud Audit Logs to show which pipelines still authenticate via keys versus federation, turning what could be an invisible, dragging migration into something leadership can actually track.
Plan for a rollback path during the first few migrations, even though you will rarely need it. Keep the old key’s binding intact but disabled, not deleted, for the first production cutover, so a bad attribute condition or a missing IAM role does not turn into a multi-hour outage while you debug it live. Once a pipeline has run successfully on the new setup for a full release cycle, that safety net stops earning its keep and the key should come out permanently.
Frequently Asked Questions
Does workload identity federation work with GitLab CI, not just GitHub Actions?
Yes. GitLab CI has issued OIDC tokens for its jobs for several years, and the GCP-side setup is nearly identical: create a provider pointed at GitLab’s issuer URL instead of GitHub’s, map the relevant claims such as project path and ref, and bind the same way.
Can I use workload identity federation from AWS Lambda or EC2 to call GCP APIs?
Yes. AWS-based workloads can federate using their IAM role credentials instead of a JSON key, following the same pool-and-provider pattern but with an AWS-specific provider type rather than OIDC.
How long does a token from workload identity federation actually last?
Roughly one hour by default, matching standard GCP access token lifetimes. The federated exchange happens fresh at the start of each job, so long-running jobs may need to refresh mid-run using the same auth action.
What happens to existing service account keys after I set this up?
Nothing automatically. Keys keep working until you manually revoke them. Migration only pays off in reduced risk once you actually delete the old keys, not just once the new pipeline works.
Is workload identity federation free to use?
Yes, there is no separate charge for pools, providers, or token exchanges. You pay only for the underlying GCP resources your pipeline touches, same as with a service account key.
Can a single provider support multiple GitHub repositories?
Yes, by scoping the IAM binding to attribute.repository_owner instead of a single repository, or by creating multiple principalSet bindings against one provider, each scoped to a different repo.
Do I still need workload identity federation if my workloads only run inside GCP, like on GKE or Cloud Functions?
For workloads entirely inside GCP, GKE Workload Identity and default service account attachment already solve the same problem without an external OIDC provider. Federation specifically matters when an external system, like GitHub or another cloud, needs to reach into GCP.
What is the biggest sign a workload identity federation setup is misconfigured?
An attribute condition that references only repository_owner for a production deploy account, or no condition at all. Either one means more repositories can impersonate that identity than actually should be able to.


