Service account keys stored as CI/CD secrets are a common source of credential leaks in GCP environments. They persist long after the pipeline that needed them has changed, get copied to local machines for testing, and when they leak — through a repository accident, a compromised CI runner, or an inadvertent log — they work from anywhere until rotated.
Workload Identity Federation (WIF) eliminates the key. Instead of storing a credential, CI systems authenticate using their native identity mechanism (OIDC tokens) and exchange that for short-lived GCP credentials on demand.
How It Works
The flow:
- GitHub Actions (or GitLab CI, etc.) generates a short-lived OIDC token proving “I am workflow X in repository Y, triggered by event Z”
- That token is exchanged with GCP’s Workload Identity Pool for a short-lived GCP access token
- The access token is scoped to the service account you’ve configured
- The token expires after one hour — it can’t be reused outside that window
No key file. No secret to rotate. The credential is minted fresh for each pipeline run and expires automatically.
Setting Up for GitHub Actions
The GCP side requires a Workload Identity Pool and Provider:
# Create the identity poolgcloud iam workload-identity-pools create github \ --project=YOUR_PROJECT_ID \ --location=global \ --display-name="GitHub Actions Pool"
# Add GitHub as an OIDC providergcloud iam workload-identity-pools providers create-oidc github \ --project=YOUR_PROJECT_ID \ --location=global \ --workload-identity-pool=github \ --display-name="GitHub Actions Provider" \ --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \ --issuer-uri="https://token.actions.githubusercontent.com"The attribute-mapping translates GitHub’s OIDC token claims to GCP attributes. assertion.repository becomes an attribute you can use in conditions — critical for ensuring only your repositories can use this pool.
Grant the service account access from the pool, restricted to specific repositories:
gcloud iam service-accounts add-iam-policy-binding \ wlif-github-actions@YOUR_PROJECT_ID.iam.gserviceaccount.com \ --project=YOUR_PROJECT_ID \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.repository/your-org/your-repo"The principalSet binding scopes access to a specific repository. A compromised token from a different repository in the same GitHub organization cannot impersonate this service account.
The GitHub Actions Workflow
Once the GCP side is configured, the workflow is three lines:
permissions: id-token: write # Required for OIDC token generation contents: read
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: google-github-actions/auth@v2 with: workload_identity_provider: 'projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/github/providers/github' service_account: 'wlif-github-actions@YOUR_PROJECT_ID.iam.gserviceaccount.com'
- name: Run dbt run: dbt run --target prodThe google-github-actions/auth action handles the OIDC token exchange. After it runs, all subsequent steps in the job have GCP credentials available via Application Default Credentials — the same mechanism that any GCP SDK uses.
Naming the Service Account
Following the per-workload naming convention, the wlif- prefix marks this as a Workload Identity Federation service account. This makes it immediately identifiable in audit logs and IAM policy listings. When you see wlif-github-actions in a BigQuery JOBS query, you know it’s a CI pipeline running against production.
Create separate service accounts for separate pipelines:
wlif-github-actions@project.iam.gserviceaccount.com— for application deploymentswlif-dbt-ci@project.iam.gserviceaccount.com— for dbt CI runswlif-terraform-ci@project.iam.gserviceaccount.com— for infrastructure changes
Each gets only the permissions its pipeline needs. The dbt CI service account needs bigquery.dataViewer to run tests; it doesn’t need bigquery.dataEditor. The Terraform CI account needs IAM admin permissions; the dbt account definitely doesn’t.
GitLab CI and Other Platforms
GitHub Actions is the most common case, but WIF supports any OIDC-compatible identity provider. GitLab, CircleCI, and most other CI platforms support OIDC authentication against external providers.
The GCP configuration is nearly identical — create an OIDC provider pointing at the platform’s token endpoint, configure attribute mapping for the relevant claims (repository, project, branch), and bind service account access to specific attribute values.
The workflow-side configuration varies by platform. Check the platform’s documentation for how to generate OIDC tokens and pass them to external services. The key invariant: you’re always exchanging a platform-native token for short-lived GCP credentials, never storing a key.
What to Do with Existing Keys
Once WIF is configured and working, the existing service account keys can be deleted. Not rotated — deleted. They’re no longer needed.
# List keys for the service accountgcloud iam service-accounts keys list \ --iam-account=wlif-github-actions@YOUR_PROJECT_ID.iam.gserviceaccount.com \ --filter="keyType=USER_MANAGED"
# Delete each key (replace KEY_ID with the actual key ID)gcloud iam service-accounts keys delete KEY_ID \ --iam-account=wlif-github-actions@YOUR_PROJECT_ID.iam.gserviceaccount.comRun the service account key audit after migrating all pipelines to confirm no keys remain. The goal state is zero user-managed keys across all workload service accounts.