Skip to main content

Command Palette

Search for a command to run...

Eliminate GCP Service Account Keys with Workload Identity

Published
5 min read

Eliminating GCP Service Account Keys with Workload Identity Federation

Static service account keys remain the single biggest identity risk in GCP environments. I find them in production across most new client engagements—embedded in CI/CD environment variables, committed to code repositories, sitting on developer laptops. Each one is a credential that never expires until someone remembers to rotate it.

Workload Identity Federation eliminates keys entirely by letting external identities exchange OIDC tokens for short-lived GCP credentials. This guide walks through implementing WIF for GitHub Actions and GKE workloads, plus service account impersonation for developers—the complete pattern for keyless GCP operations.

Why This Matters

A compromised GCP API key cost one team I advised $450k in a single weekend. Someone pushed a key to a public GitHub repo. Attackers spun up Cloud Run services in every region within hours. The key had no expiry, no geographic restrictions, and far more permissions than the workload actually needed.

WIF makes this attack vector impossible. No static credential exists to steal.

Prerequisites

Before starting:

  • GCP project with billing enabled
  • gcloud CLI authenticated with permissions to create IAM resources
  • For GitHub Actions: repository where you control workflow files
  • For GKE: existing cluster with Workload Identity enabled (or ability to enable it)
  • Organization-level access if you want to enforce the disableServiceAccountKeyCreation constraint

Step 1: Create a Dedicated Service Account

Why this matters: The service account defines your workload's permission boundary. Every external identity (GitHub Actions runner, Kubernetes pod) will ultimately operate with this SA's permissions.

gcloud iam service-accounts create github-actions-deploy \
  --display-name="GitHub Actions Deployment SA" \
  --project=my-project-id

Grant only the permissions the workload actually needs:

gcloud projects add-iam-policy-binding my-project-id \
  --member="serviceAccount:github-actions-deploy@my-project-id.iam.gserviceaccount.com" \
  --role="roles/run.developer"

What goes wrong: Teams often copy permissions from an existing over-privileged SA. Start with zero permissions and add only what's required. I've seen service accounts with roles/owner being federated—the WIF pattern is secure, but it can't fix bad permission boundaries.

Step 2: Create a Workload Identity Pool

Why this matters: The pool is a container for identity providers. You'll have one pool with potentially multiple providers (GitHub, GitLab, AWS, etc.).

gcloud iam workload-identity-pools create github-pool \
  --location="global" \
  --display-name="GitHub Actions Pool" \
  --project=my-project-id

What goes wrong: Teams create separate pools per repository. This creates management overhead. One pool per identity provider type is usually sufficient—use attribute conditions to scope access per repository.

Step 3: Add GitHub as an Identity Provider

Why this matters: This tells GCP how to validate tokens from GitHub Actions and what claims to trust.

gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location="global" \
  --workload-identity-pool="github-pool" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.actor=assertion.actor" \
  --attribute-condition="assertion.repository=='my-org/my-repo'" \
  --project=my-project-id

The --attribute-condition is critical. Without it, any GitHub Actions workflow from any repository could impersonate your service account.

What goes wrong: The attribute condition syntax trips people up. Use single quotes inside the condition string. Test with a permissive condition first, then tighten it once tokens are flowing.

Step 4: Bind the Service Account to the Workload Identity

Why this matters: This is the authorization step—allowing the federated identity to impersonate the GCP service account.

gcloud iam service-accounts add-iam-policy-binding \
  github-actions-deploy@my-project-id.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/my-org/my-repo" \
  --project=my-project-id

Replace PROJECT_NUMBER with your numeric project number (not the project ID).

What goes wrong: Using project ID instead of project number is the most common error. The binding silently succeeds but authentication fails. Find your project number with:

gcloud projects describe my-project-id --format="value(projectNumber)"

Step 5: Configure GitHub Actions Workflow

Why this matters: The workflow authenticates to GCP without any stored secrets.

name: Deploy to Cloud Run

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider
          service_account: github-actions-deploy@my-project-id.iam.gserviceaccount.com

      - uses: google-github-actions/setup-gcloud@v2

      - run: gcloud run deploy my-service --source .

What goes wrong: Missing permissions.id-token: write causes silent failures. The auth action can't request the OIDC token without this permission explicitly granted.

Step 6: Configure GKE Workload Identity

For Kubernetes workloads, the pattern connects a Kubernetes Service Account to a GCP Service Account.

Why this matters: Pods authenticate to GCP APIs without mounting key files or setting environment variables.

# Create the GCP service account
gcloud iam service-accounts create gke-workload-sa \
  --project=my-project-id

# Bind the Kubernetes SA to the GCP SA
gcloud iam service-accounts add-iam-policy-binding \
  gke-workload-sa@my-project-id.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:my-project-id.svc.id.goog[my-namespace/my-ksa]"

Annotate the Kubernetes Service Account:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-ksa
  namespace: my-namespace
  annotations:
    iam.gke.io/gcp-service-account: gke-workload-sa@my-project-id.iam.gserviceaccount.com

What goes wrong: The namespace must match exactly. I've seen pods fail silently with permission denied because the KSA was in default but the binding specified production. Check the binding member string character by character.

Step 7: Enable Service Account Impersonation for Developers

Why this matters: Developers need GCP access for local development without holding permanent keys.

# Grant impersonation permission to a developer
gcloud iam service-accounts add-iam-policy-binding \
  dev-sa@my-project-id.iam.gserviceaccount.com \
  --role="roles/iam.serviceAccountTokenCreator" \
  --member="user:developer@company.com"

Developers configure impersonation:

gcloud config set auth/impersonate_service_account dev-sa@my-project-id.iam.gserviceaccount.com

What goes wrong: Audit trails only capture impersonation events if Cloud Audit Logs include sts.googleapis.com data access logs. Enable these explicitly or you'll have gaps in your audit trail during SOC 2 reviews.

Step 8: Enforce Keyless Operations with Org Policy

Why this matters: This is the Security-by-Design principle from the SCALE framework—making insecure patterns impossible rather than relying on policy compliance.

gcloud org-policies set-policy policy.yaml --project=my-project-id

Where policy.yaml contains:

name: projects/my-project-id/policies/iam.disableServiceAccountKeyCreation
spec:
  rules:
    - enforce: true

What goes wrong: Applying this policy without WIF already working causes immediate CI/CD failures. Migrate all workloads first, verify they're functioning, then enable the constraint.

Common Mistakes

Leaving old keys active during migration. I've seen teams complete WIF setup but keep old keys "just in case." Audit trails show both being used months later. Delete old keys once WIF is verified working.

Cross-project impersonation chains. Keep impersonation to one hop maximum. Service account A impersonating B impersonating C creates debugging nightmares and audit complexity.

Over-scoped attribute conditions. An empty --attribute-condition lets any token from that provider authenticate. Always scope to specific repositories, branches, or environments.

Every SOC 2 audit I've supported has flagged service account keys as a finding. The migration path from keys to WIF is well-defined—the blocker is usually organizational, not technical.


Work with a GCP specialist — book a free discovery call

Amit Malhotra, Principal GCP Architect, Buoyant Cloud Inc


Work with a GCP specialist — book a free discovery callhttps://buoyantcloudtech.com