In the world of CI/CD, one thing we are all tired of is managing long-lived service account keys. They are hard to rotate.. risky to store, and can lead to serious breaches if leaked! πΏ
In this post, I will be showing how to set up GitHub Actions + Google Cloud Workload Identity Federation (WIF) to enable secure, short-lived authentication, with no secrets stored anywhere. π«π
π¨ The Problem with Static Secrets
It is still common to authenticate from GitHub Actions to GCP using service account keys stored as GitHub secrets:
echo "${{ secrets.GCP_KEY }}" > key.json
gcloud auth activate-service-account --key-file=key.json
This approach works, but it has downsides such as:
- π Long-lived credentials: valid for months or years
- π₯ Risk of leakage
- π Manual rotation or forgotten rotation π¬
- π¦ Secrets management overhead
β The Better Way: Workload Identity Federation π
Instead of storing credentials, we can let GitHub Actions request an identity token at runtime, and have GCP validate that token and issue a short-lived access token.
This is made possible using:
- OpenID Connect (OIDC): A standard that GitHub supports natively
- Workload Identity Federation: A GCP feature that lets you trust identities from external providers (like GitHub)
π οΈ How It Works
Letβs walk through the high-level flow!
Source: Google Cloud Blog
- β GitHub Actions requests an OIDC token signed by GitHub
- π This token is sent to GCPβs Security Token Service (STS)
- π GCP verifies the token and exchanges it for a short-lived access token
- π Your Action uses this token to call GCP APIs & no secrets involved
π§© Setting It Up (High-Level)
- Create a Workload Identity Pool & Provider in GCP
gcloud iam workload-identity-pools create github-pool \
--location="global" \
--display-name="GitHub Actions Pool"
gcloud iam workload-identity-pools providers create-oidc github-provider \
--location="global" \
--workload-identity-pool="github-pool" \
--display-name="My GitHub Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com"
- Create a Service Account and Allow GitHub to Impersonate It
gcloud iam service-accounts create gha-deploy
gcloud iam service-accounts add-iam-policy-binding gha-deploy@YOUR_PROJECT.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"
- Configure GitHub Actions Please see: https://github.com/google-github-actions/auth
In your workflow:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: 'actions/checkout@v4'
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
token_format: "access_token"
workload_identity_provider: "projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
service_account: "gha-deploy@YOUR_PROJECT.iam.gserviceaccount.com"
Now you now have short-lived credentials, scoped precisely to your repository and branch π
π Locking It Down: Use GitHub OIDC Attributes for Fine-Grained Access
GitHub’s OIDC tokens come with a set of useful claims. When you configure your provider in GCP, you can map these claims to attributes, and then use those attributes to control who gets access to impersonate your service account.
π Common GitHub Claims
Claim | Description |
---|---|
sub | e.g. repo:my-org/my-repo:ref:refs/heads/main |
repository | my-org/my-repo |
ref | refs/heads/main, refs/tags/v1.0.0, etc |
workflow | .github/workflows/deploy.yml |
job_workflow_ref | my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main |
actor | The GitHub user who triggered the workflow π |
πΊοΈ Example Attribute Mapping
You can map these into usable GCP attributes when creating the provider:
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref,attribute.workflow=assertion.workflow,attribute.actor=assertion.actor"
π Use Conditions to Restrict Access
Now you can enforce specific conditions when assigning access:
π Restrict to a specific repo
principalSet://.../attribute.repository/my-org/my-repo
π Restrict to a repo and branch
attribute.repository == "my-org/my-repo" &&
attribute.ref == "refs/heads/main"
π Restrict to a specific workflow file
attribute.workflow == ".github/workflows/deploy.yml"
π Restrict to a specific actor (user/bot)
attribute.actor == "deploy-bot"
π§± Putting It in Practice
gcloud iam service-accounts add-iam-policy-binding [email protected] \
--role="roles/storage.admin" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/my-org/my-repo" \
--condition="expression=attribute.ref=='refs/heads/main' && attribute.workflow=='.github/workflows/deploy.yml'"
Now only your deploy.yml
workflow on the main branch can assume this identity. πβ¨
π§ Federation β Impersonation
Workload Identity Federation lets GCP trust an external identity (like GitHub) without the external system needing any GCP credentials. Think of it like: βGitHub says this workflow is running, and GCP trusts that, so GCP hands back a short-lived access token.β π€π»
It’s similar to assuming a role in AWS, but uses open standards like OIDC and token exchange (STS).
π― Benefits Recap
- β No stored secrets
- β Short-lived, scoped credentials
- β Fine-grained access with identity claims
- β Multi-cloud ready (also available in AWS, Azure)
π Final Thoughts
This pattern is fast becoming the new standard for CI/CD authentication in cloud-native platforms. Itβs simpler, safer, and aligns well with platform engineering principles.
If your team is still managing static service account keys in CI β give this a try π