Skip to content

09 β€” Workload Identity Federation (Keyless GitHub Actions Auth)

← Previous: 08 β€” Custom Domain & SSL

βœ… Free. Workload Identity Federation has no cost.

Why does GitHub Actions need to authenticate at all?

When GitHub Actions runs your CI/CD pipeline, it's just a Linux machine on GitHub's servers with no special relationship to your GCP project. But the pipeline needs to do privileged things: push Docker images to Artifact Registry, trigger Cloud Run deployments, and update Cloud Run Jobs. All of these are protected GCP API calls β€” Google needs to verify that the caller is actually allowed to touch your project before it does anything.

Without authentication, every gcloud run deploy or docker push command would fail with a 403 Permission denied error. This chapter sets up that authentication.

The problem with service account keys

The naive way to authenticate GitHub Actions to GCP is to create a JSON key file for the service account and paste it into a GitHub secret. This works but has risks: the key file never expires, if it leaks it gives full access until manually revoked, and rotating it requires manual steps.

What is Workload Identity Federation?

Workload Identity Federation lets GitHub Actions prove its identity to GCP using a short-lived OIDC token (like a temporary ID card) instead of a permanent key. The flow is:

Workload Identity Concept Map

  1. GitHub Actions requests a signed JWT from GitHub's OIDC provider, proving "I am a workflow running in repo YOUR_ORG/YOUR_REPO on branch main"
  2. GCP's Workload Identity Pool verifies the JWT signature against GitHub's public keys
  3. GCP issues a short-lived GCP access token (valid for ~1 hour)
  4. The workflow uses that token to push images and deploy

No permanent credentials are ever stored anywhere.


Setup (one-time, run in your local terminal)

# Fetches the numeric project ID β€” needed to construct the Workload Identity resource name.
PROJECT_NUMBER=$(gcloud projects describe mycoolproject-prod --format='value(projectNumber)')

# Creates a Workload Identity Pool β€” a container for external identity providers.
# Think of it as a trust boundary: only providers inside this pool can exchange tokens.
# Result: visible at console.cloud.google.com/iam-admin/workload-identity-pools
gcloud iam workload-identity-pools create github-pool \
  --location=global \
  --display-name="GitHub Actions Pool"

# Creates an OIDC provider inside the pool that trusts GitHub Actions' JWT tokens.
# attribute.repository maps the JWT's repo claim so we can restrict to specific repos.
# This tells GCP: "accept short-lived tokens signed by GitHub's OIDC issuer".
gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location=global \
  --workload-identity-pool=github-pool \
  --display-name="GitHub provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

Attribute mapping explained:

  • google.subject=assertion.sub β€” maps the JWT's sub field to GCP's subject
  • attribute.repository=assertion.repository β€” exposes the repo name as a GCP attribute so we can restrict to specific repos
# Grants workflows from YOUR_ORG/YOUR_REPO permission to impersonate mycoolproject-run-sa.
# This is the key security boundary: only your repo can become that service account.
# YOUR_ORG/YOUR_REPO format is just: username/reponame β€” no github.com prefix, no .git suffix.
# Example: oliverm91/django-gcp-deployment-guide
gcloud iam service-accounts add-iam-policy-binding \
  mycoolproject-run-sa@mycoolproject-prod.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/YOUR_ORG/YOUR_REPO"

This binding says: "workflows running from YOUR_ORG/YOUR_REPO may act as mycoolproject-run-sa". Workflows from any other repo cannot.

# Prints the full provider resource name β€” copy this value into the
# GCP_WORKLOAD_IDENTITY_PROVIDER GitHub secret in the next step.
gcloud iam workload-identity-pools providers describe github-provider \
  --location=global \
  --workload-identity-pool=github-pool \
  --format="value(name)"

The output looks like:

projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider

Copy this entire string β€” this is what you paste as the GCP_WORKLOAD_IDENTITY_PROVIDER secret value in the next step.


Add GitHub Secrets

Go to GitHub β†’ your repo β†’ Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret:

Secret name Value
GCP_WORKLOAD_IDENTITY_PROVIDER The full string from the last command, e.g. projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider
GCP_SERVICE_ACCOUNT mycoolproject-run-sa@mycoolproject-prod.iam.gserviceaccount.com

These are the only two values GitHub needs. No JSON key file, no password.


How the workflow uses them

In the GitHub Actions workflow (chapter 10), this step exchanges the GitHub OIDC token for a GCP access token:

- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
    service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

After this step, gcloud and docker commands in the workflow automatically use the GCP credentials. The token expires when the workflow ends.