If your delivery pipeline still depends on JSON service account keys stored in GitHub secrets, GitLab variables, or CI runners, your problem is not convenience. Your problem is blast radius. Google recommends using Workload Identity Federation for deployment pipelines such as GitHub Actions and GitLab because it replaces long-lived keys with short-lived credentials derived from trusted external identities.

The security shift is simple: instead of exporting a Google credential into your CI system, you let the CI system prove who it is with an OIDC token, then exchange that proof through Google Cloud Security Token Service, and finally impersonate a narrowly scoped service account. That means no static key to leak, rotate, forget, or accidentally reuse across environments.

The target architecture

A secure migration does not start with YAML. It starts with a trust model:

  1. Your pipeline platform issues an OIDC token for a specific workflow or job.
  2. Google Cloud trusts only a specific workload identity pool and provider.
  3. A tightly filtered external identity is allowed to impersonate exactly one service account.
  4. That service account has only the roles required for one deployment path.

This matters because Workload Identity Federation can still be misconfigured. Google's own guidance is explicit: use immutable attributes in mappings, choose a unique google.subject, avoid granting access to all identities in a pool, and restrict impersonation to specific subjects, groups, or attributes. Otherwise you replace key sprawl with federation sprawl.

Step 1: Inventory every key before you remove any key

Before migrating, list every place where a service account key is used:

  • GitHub repository secrets
  • GitHub organization secrets
  • GitLab CI/CD variables
  • self-hosted runners
  • Terraform variables such as GOOGLE_CREDENTIALS or GOOGLE_APPLICATION_CREDENTIALS
  • bootstrap scripts and local wrapper scripts

Do not start by creating one large shared pool and one shared deployer service account. Instead, map each pipeline to an identity boundary: repository, branch, environment, project, and deployment target. Google recommends dedicated service accounts per application and keeping service accounts in the same project as the resources they access, because that makes least privilege easier to enforce and to retire later.

Step 2: Build the pool and provider around claims you can trust

For GitHub Actions and GitLab, the common pattern is the same: create a workload identity pool, add an OIDC provider, map claims into Google attributes, and then bind only the identities you intend to trust. Google's deployment-pipeline guidance explicitly supports GitHub OIDC tokens and GitLab job ID tokens for this flow.

The design choice that separates a secure setup from a fragile one is which claim becomes your trust anchor. For GitHub, repository, repository owner, ref, environment, and reusable workflow context are useful claims; GitHub also requires id-token: write in the workflow so a job can request an OIDC token. For GitLab, the job can mint an ID token via id_tokens, and Google-side authorization is commonly expressed with principalSet:// members tied to mapped attributes.

The practical rule is this: bind to immutable identity, then narrow by environment. A repository ID is stronger than a mutable repository name. A workspace or project path is stronger than a human-readable label. A subject that uniquely maps to one external identity is stronger than a generic string you will not be able to trace later in audit logs. Google explicitly recommends unique subject mappings so suspicious activity can be correlated back to the original external identity.

Step 3: Bind a federated principal to one service account, not to your project

In most production setups, the external identity should not receive broad project permissions directly. Instead, it should receive roles/iam.workloadIdentityUser on a single service account, and that service account should hold the minimal resource roles required for deploy, plan, or read-only actions. Google documents this service-account impersonation pattern for workload federation and notes that principal identifiers must use the project number, not the project ID.

That distinction is where many migrations fail. Teams create the pool correctly, then grant one service account to an entire pool, or one deployer account to every environment. The safer pattern is:

  • one service account per delivery path
  • one trust condition per repository / project / workspace
  • separate identities for plan and apply when Terraform is involved
  • explicit environment segmentation for staging and production

GitHub Actions: the cleanest migration path

GitHub's recommended OIDC model is straightforward: grant id-token: write, use Google's authentication action, and exchange the GitHub OIDC token for short-lived Google credentials instead of storing a JSON key in secrets. The Google GitHub Action explicitly recommends Workload Identity Federation over service account keys because keys are long-lived credentials that must be treated like passwords.

A minimal workflow shape looks like this:

permissions:
  contents: read
  id-token: write

steps:
  - uses: actions/checkout@v4
  - uses: google-github-actions/auth@v3
    with:
      workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
      service_account: deployer@PROJECT_ID.iam.gserviceaccount.com
  - uses: google-github-actions/setup-gcloud@v2
  - run: gcloud run deploy ...

Two operational notes matter. First, GitHub's OIDC token is short-lived, and the Google auth action documents a five-minute lifetime for the GitHub token at the time of writing, so long-running workflows should authenticate close to the deployment step rather than at job start. Second, if you still rely on gsutil, the auth action notes that gsutil does not automatically use the exported credentials; gcloud storage is the safer default in this model.

GitLab: same destination, different syntax

GitLab's flow is conceptually identical: a job gets an ID token, Google Cloud verifies it through the workload identity provider, and the job obtains a temporary token for service account impersonation. GitLab's documentation is explicit that this setup generates short-lived credentials without storing secrets.

A minimal GitLab shape looks like this:

deploy:
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  script:
    - echo "exchange token with GCP STS or use generated credential config"
    - gcloud run deploy ...

The security value in GitLab is not the YAML itself. It is the binding strategy on the Google side. GitLab shows how external identities can be granted impersonation rights with principalSet:// members based on mapped attributes such as user_login. In real systems, you usually want to bind on project, namespace, environment, or protected deployment path rather than on a broad user attribute alone.

Terraform: treat plan and apply as different trust domains

Terraform is where many teams quietly reintroduce key risk. They remove keys from the deployment job, then keep a JSON key for terraform plan, remote state access, or a separate automation runner.

There are two secure patterns. If Terraform runs inside GitHub Actions or GitLab, let it inherit the same federated authentication from the pipeline and remove GOOGLE_CREDENTIALS and GOOGLE_APPLICATION_CREDENTIALS from the environment. If Terraform runs in HCP Terraform, HashiCorp supports dynamic provider credentials for GCP using OIDC, and Google's deployment-pipeline guidance notes that direct resource access is not supported for HCP Terraform, so service account impersonation is the expected pattern there. HashiCorp also supports splitting credentials by plan and apply, which is one of the best ways to reduce standing privilege in infrastructure pipelines.

Rollback plan

A serious migration keeps rollback boring:

  1. enable federation first
  2. validate one non-production deployment path
  3. keep the old key disabled but recoverable for a short window
  4. confirm audit logs show the expected federated subject and service account chain
  5. delete old keys only after two or three successful production runs

Notice the sequence: disable first, delete later. That gives you a reversible safety net without preserving a quietly active secret forever. Google's guidance on service account keys is blunt: they are a security risk if not managed correctly, and you should choose a more secure alternative whenever possible.

Common failure modes

Most failed WIF rollouts come from five mistakes:

  • trusting broad pool membership instead of binding to specific attributes
  • using mutable names where immutable claims are available
  • reusing one service account across multiple apps or environments
  • forgetting GitHub id-token: write or GitLab id_tokens
  • leaving old JSON keys in secrets "just in case"

The point of this migration is not to modernize authentication syntax. The point is to make credential theft materially harder and privilege boundaries materially smaller.

In 2026, "we rotate service account keys" is no longer a strong cloud security position. It is merely a cleaner version of the same weakness. The mature posture is keyless CI/CD: short-lived tokens, strict claim conditions, narrow impersonation, dedicated service accounts, and no standing secrets in the delivery path. Google Cloud already gives you the primitives. The work now is architectural discipline.

🙏 If you found this article helpful, give it a 👏 and hit Follow — it helps more people discover it.

🌱 Good ideas tend to spread. I truly appreciate it when readers pass them along.

📬 I also write more focused content on JavaScript, React, Python, DevOps, and more — no noise, just useful insights. Take a look if you're curious.