Workload Identity Federation
Workload Identity Federation — WIF — lets an external workload authenticate to GCP using its own identity token, with no service-account key anywhere. A GitHub Actions job, a GitLab pipeline, or another cloud's workload presents the OIDC token it already has, and GCP exchanges it for a short-lived GCP token gated by a workload identity pool you configure in Terraform.
This is how keyless CI/CD to GCP is done. The old answer — a JSON service-account key pasted into a CI secret — is a long-lived credential that leaks, gets copied across forks, and never rotates. WIF removes it entirely: there is nothing to store, because the pipeline authenticates with the OIDC token it is issued fresh on every run.
The Keyless CI Problem
Pipelines need to act on GCP — Hatch's hatch-io/infrastructure repo runs terraform apply from GitHub Actions. The old way was a service-account JSON key dropped into the repo's Actions secrets. That key is a standing liability: it sits in a settings page indefinitely, copies into every fork, and is the single most common avoidable GCP CI security hole.
WIF deletes the problem rather than managing it. There is no key to store, rotate, or leak, because the credential is the OIDC token GitHub mints for the workflow run itself — short-lived, scoped, and never written down. Remove the trust and the access is gone instantly; no key hunt required.
Pools and Providers
Two resources describe the trust. A google_iam_workload_identity_pool is the container, and a google_iam_workload_identity_pool_provider inside it declares which external issuer to trust — GitHub's OIDC endpoint — and how to map its token claims onto GCP attributes like repository and ref.
resource "google_iam_workload_identity_pool" "github" { workload_identity_pool_id = "hatch-github-pool" } resource "google_iam_workload_identity_pool_provider" "github" { workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id workload_identity_pool_provider_id = "github-oidc" attribute_mapping = { "google.subject" = "assertion.sub" "attribute.repository" = "assertion.repository" } oidc { issuer_uri = "https://token.actions.githubusercontent.com" } }
The pool holds the trust; the provider names GitHub's issuer and maps the token's repository claim onto a GCP attribute. That mapped attribute is what the next section pins trust to — without it, the pool would trust any token GitHub issues.
The Trust Mapping
An attribute condition pins trust to a specific repository so that only your pipeline — not any GitHub repo on the planet — can authenticate. The condition is a boolean expression over the mapped attributes, and the one that matters is the repository check: assertion.repository == 'hatch-io/infrastructure'.
resource "google_iam_workload_identity_pool_provider" "github" { # ... pool id, mapping, oidc as above ... attribute_condition = "assertion.repository == 'hatch-io/infrastructure'" }
Without this condition the provider trusts every token GitHub's OIDC endpoint issues — meaning any repo, anywhere, could mint GCP tokens through your pool. The condition narrows that to exactly hatch-io/infrastructure. Where a branch matters, add a ref check too — but always pin the repository, or a fork with a matching branch name slips through.
Binding to a Service Account
The federated identity is then granted roles/iam.workloadIdentityUser on the service account it should act as, so the pipeline runs as that SA with exactly its permissions. The binding's member references the pool and the mapped attribute, tying the GitHub repo to one GCP service account.
resource "google_service_account_iam_member" "wif" { service_account_id = google_service_account.ci_deployer.name role = "roles/iam.workloadIdentityUser" member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/hatch-io/infrastructure" }
The principalSet member ties the hatch-io/infrastructure repo to the ci_deployer service account. Forget this binding and the pool authenticates the token but has no SA to map it onto, producing an authentication failure after everything else looks configured. Scope ci_deployer to least privilege — the pipeline gets exactly its roles, nothing more.
The GitHub Actions Flow
At runtime the pieces connect in three steps. The Actions runner requests an OIDC token for the job; the google-github-actions/auth action exchanges that token against the pool for a short-lived GCP access token on ci_deployer; and Terraform then runs as that service account. No key ever touches the runner.
The deep mechanics of the workflow YAML and the auth action belong to the CI/CD chapter, where Hatch's full pipeline is built. Here the point is the GCP-side trust: the pool, the provider, the attribute condition, and the binding are what you manage in Terraform so the whole relationship is reviewable in a pull request.
Service-account key in CI — a long-lived JSON secret sitting in a settings page, copied across forks, and almost never rotated. Anyone who reads the secret holds the account's full power until someone notices and rotates the key.
Workload Identity Federation — trades the pipeline's own short-lived OIDC token for a GCP token scoped to one repo and branch, with nothing stored anywhere. There is no good reason to use keys for GCP CI anymore.
- Storing a service-account JSON key in GitHub Actions secrets instead of configuring WIF — the single most common avoidable GCP CI security hole.
- Writing an over-broad attribute condition that trusts the whole GitHub org instead of one repo — every repo in the org can then mint GCP tokens through your pool.
- Forgetting the
workloadIdentityUserbinding on the target SA and getting an authentication failure even though the pool and provider look correct. - Pinning trust to a branch but not the repository, so a fork with a matching branch name can authenticate against your pool.
- Binding the federated identity to a broadly-scoped service account, so a compromised pipeline inherits far more access than it needs.
- Use Workload Identity Federation for all GCP CI/CD; never store service-account keys in a pipeline.
- Constrain the
attribute_conditionto the exact repository and, where it matters, the branch or environment. - Grant the federated identity
roles/iam.workloadIdentityUseron a least-privileged service account, not a broadly-scoped one. - Manage the pool, provider, and bindings in Terraform so the trust relationship is reviewable in a pull request.
- Always pin the repository in the condition, not only the branch, so no fork can borrow the trust.
Knowledge Check
What does Workload Identity Federation replace, and why is that an improvement?
- A long-lived service-account key in CI secrets, with a short-lived OIDC token exchange that stores nothing
- The need to enable any of the required GCP service APIs before running Terraform
- The provider block's default project and region configuration settings, which federation rewrites on each run so the workload always lands in the right place
- The state file, by moving the long-lived credentials into state instead
What does the attribute condition pin trust to, and what goes wrong if it is too broad?
- The specific repository (and optionally branch); too broad and any repo in the org can mint tokens through your pool
- The GCP region the workload runs in; too broad and resources land in the wrong location
- The allowed provider version range; too broad and the lock file silently drifts onto a newer build that no one on the team reviewed before it shipped
- The billing account charged for the run; too broad and the costs are misattributed
Which role binds the federated identity to the service account it should act as?
roles/iam.workloadIdentityUseron the target service accountroles/iam.serviceAccountKeyAdminon the identity pool resourceroles/owneron the enclosing projectroles/iam.workloadIdentityPoolAdminon the CI runner
You pin trust to a branch but not the repository. What is the security hole?
- A fork with a matching branch name can authenticate against your pool
- The pool stops accepting any tokens at all until that branch is deleted and recreated
- The issued OIDC token expires almost immediately on every single run
- Terraform silently falls back to a long-lived service-account key automatically
You got correct