cloud-devops

GitHub Actions OIDC to Google Cloud: Keyless Deploys (2026)

June 5, 2026

GitHub Actions OIDC to Google Cloud: Keyless Deploys (2026)

To authenticate GitHub Actions to Google Cloud without a service account key, create a Workload Identity Pool and OIDC Provider trusting https://token.actions.githubusercontent.com, restrict admission with an attribute condition, and exchange the workflow's OIDC token via google-github-actions/auth@v3. This tutorial wires it end to end into a Cloud Run deploy.

TL;DR

You will configure Google Cloud Workload Identity Federation (WIF) so a GitHub Actions workflow can deploy to Cloud Run with zero stored credentials — no JSON key in your repo secrets, nothing to rotate, nothing to leak. The whole setup is ten gcloud commands plus one workflow file using google-github-actions/auth@v31 and google-github-actions/deploy-cloudrun@v32, and takes about 20 minutes. You'll also future-proof your trust config for GitHub's immutable subject claims, which become the default for new repositories on June 18, 20263.

What you'll learn

  • How GitHub Actions OIDC token exchange with Google Cloud works (and how it differs from the AWS flow)
  • How to create a Workload Identity Pool and Provider with safe attribute conditions
  • Direct Workload Identity Federation vs. service account impersonation — and which one a Cloud Run deploy needs
  • The exact IAM roles GitHub Actions needs to deploy to Cloud Run
  • The keyless deploy workflow YAML, step by step
  • How to verify the deploy and harden the trust policy for the June 18, 2026 immutable-claim change
  • Troubleshooting five documented failure modes

How the token exchange works

When a workflow job runs with the id-token: write permission, GitHub mints a short-lived OIDC token whose claims identify the workflow: repository, repository_owner, ref, and a composite sub (subject) claim that defaults to repo:OWNER/REPO:ref:refs/heads/BRANCH4. Google Cloud's Security Token Service verifies that token against GitHub's issuer (https://token.actions.githubusercontent.com), checks it against your pool's attribute condition, and exchanges it for a short-lived Google credential. The GitHub OIDC token itself is short-lived — the auth README notes it expires in about 5 minutes1.

If you followed our AWS OIDC keyless deploy tutorial, the shape is familiar — but GCP differs in two ways: trust lives in a dedicated pool + provider resource pair instead of an IAM role's trust policy, and GCP offers two distinct modes (Direct WIF and service account impersonation) where AWS has only role assumption.

sequenceDiagram
    participant GH as GitHub Actions job
    participant IdP as token.actions.githubusercontent.com
    participant STS as Google Cloud STS
    participant IAMC as IAM Credentials API
    participant CR as Cloud Run Admin API
    GH->>IdP: Request OIDC token (id-token: write)
    IdP-->>GH: Signed JWT (sub, repository, ref claims)
    GH->>STS: Exchange JWT (pool/provider validates + attribute condition)
    STS-->>GH: Federated token
    GH->>IAMC: Impersonate deployer SA (roles/iam.workloadIdentityUser)
    IAMC-->>GH: OAuth 2.0 access token
    GH->>CR: gcloud run deploy (roles/run.admin)

Direct WIF vs. service account impersonation

The auth action supports two WIF modes, and the official README labels Direct WIF "(Preferred)"1:

Direct WIFWIF through a service account
Intermediate resourceNone — the pool gets IAM grants directlyA service account is impersonated
How you select itOmit service_account inputProvide service_account input
Token lifetimeFederated token, max 10 minutesOAuth 2.0 access token; generated access tokens default to 1 hour (3600s)
CoverageNot all GCP resources accept principalSet identitiesWorks with any API the service account can call
OAuth/ID token mintingNot supportedSupported

Direct WIF is the cleaner default for resources that support federated principals (the README demonstrates Secret Manager). For this tutorial we use WIF through a service account, because that is the authorization model the deploy-cloudrun action documents: a service account with roles/run.admin plus Service Account User on the Compute Engine default service account2. You get keyless auth either way — the service account here has no exported key; it can only be impersonated by workflows that pass your attribute condition.

Prerequisites

  • A Google Cloud project with billing enabled, and the gcloud CLI installed and authenticated (gcloud auth login) as a project owner or editor
  • A GitHub repository (github.com — GitHub Enterprise Server uses a different issuer URL, see Troubleshooting)
  • Pinned action versions used below: actions/checkout@v6.0.2 (2026-01-09), google-github-actions/auth@v3.0.0 (2025-08-28), google-github-actions/deploy-cloudrun@v3.0.1 (2025-09-03)5

Set the shell variables every step uses:

export PROJECT_ID="your-project-id"
export GITHUB_ORG="your-github-org-or-username"
export REPO="your-github-org-or-username/your-repo"

Step 1 — Enable the required APIs

Workload Identity Federation needs the IAM, Security Token Service, and IAM Credentials APIs; the deploy target needs Cloud Run6:

gcloud services enable \
  iam.googleapis.com \
  sts.googleapis.com \
  iamcredentials.googleapis.com \
  run.googleapis.com \
  --project="${PROJECT_ID}"

Expected output: Operation "operations/..." finished successfully.

Step 2 — Create the Workload Identity Pool

gcloud iam workload-identity-pools create "github" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="GitHub Actions Pool"

Capture the pool's full resource name — you'll need it for IAM bindings:

export WORKLOAD_IDENTITY_POOL_ID=$(gcloud iam workload-identity-pools describe "github" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)")
echo "${WORKLOAD_IDENTITY_POOL_ID}"

Expected output (note the project number, not the project ID):

projects/123456789/locations/global/workloadIdentityPools/github

Step 3 — Create the OIDC Provider with an attribute condition

This is the trust anchor. Two rules from the official auth README are non-negotiable: always add an attribute condition restricting admission into the pool, and map every claim you want to assert on — you cannot reference assertion.repository in a condition or IAM binding unless it's mapped first1.

gcloud iam workload-identity-pools providers create-oidc "github-provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="github" \
  --display-name="GitHub repo provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'" \
  --issuer-uri="https://token.actions.githubusercontent.com"

The attribute condition is a CEL expression evaluated against the incoming token. Restricting on repository_owner keeps any repository outside your org from even entering the pool — IAM bindings then narrow access further per repository. Two gotchas: conditions are case-sensitive on the Google side even though GitHub treats names case-insensitively, and a mapped google.subject longer than 127 bytes (long repo + branch names) is rejected by IAM7.

Extract the provider resource name for the workflow:

gcloud iam workload-identity-pools providers describe "github-provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="github" \
  --format="value(name)"

Expected output:

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

This full path — with the project number — is your workload_identity_provider value. Passing the project ID instead of the number is a failure mode the official troubleshooting guide calls out explicitly7.

Step 4 — Create the deploy service account and grant roles

Create a dedicated deployer service account (no key will ever exist for it):

gcloud iam service-accounts create "github-deployer" \
  --project="${PROJECT_ID}" \
  --display-name="GitHub Actions Cloud Run deployer"

Grant it what the deploy-cloudrun action documents: Cloud Run Admin on the project, and Service Account User on the Compute Engine default service account (the runtime identity new Cloud Run revisions act as)2:

gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member="serviceAccount:github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/run.admin"

export PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")

gcloud iam service-accounts add-iam-policy-binding \
  "${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --member="serviceAccount:github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/iam.serviceAccountUser"

Step 5 — Allow your repository to impersonate the deployer

Bind the pool's federated identity for your repository only to the service account with roles/iam.workloadIdentityUser1:

gcloud iam service-accounts add-iam-policy-binding \
  "github-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

The principalSet://...attribute.repository/... member matches every workflow run whose token's repository claim equals your-org/your-repo — which is exactly why Step 3 had to map attribute.repository=assertion.repository first.

Defense now sits at two layers: the attribute condition gates which tokens enter the pool (your org), and this IAM binding gates which of those identities can impersonate the deployer (one repo).

Step 6 — The keyless deploy workflow

Create .github/workflows/deploy.yml. Replace the workload_identity_provider value with the provider resource name from Step 3:

name: deploy-cloud-run

on:
  push:
    branches: ['main']

jobs:
  deploy:
    runs-on: 'ubuntu-latest'

    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
      - uses: 'actions/checkout@v6.0.2'

      - id: 'auth'
        uses: 'google-github-actions/auth@v3.0.0'
        with:
          project_id: 'your-project-id'
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github/providers/github-provider'
          service_account: 'github-deployer@your-project-id.iam.gserviceaccount.com'

      - id: 'deploy'
        uses: 'google-github-actions/deploy-cloudrun@v3.0.1'
        with:
          service: 'hello-keyless'
          region: 'us-central1'
          image: 'us-docker.pkg.dev/cloudrun/container/hello:latest'

      - name: 'Show service URL'
        run: 'echo "Deployed to ${{ steps.deploy.outputs.url }}"'

Three details that break deploys when missed:

  1. id-token: write is mandatory. Without it, GitHub never injects the OIDC token and the auth step fails. Note that adding a permissions block removes the defaults, so contents: read must be restated for checkout1.
  2. actions/checkout must come before auth. The action writes a credentials file into $GITHUB_WORKSPACE for later steps; the README is explicit that omitting checkout or placing it after auth leaves future steps unable to authenticate1.
  3. Add gha-creds-*.json to .gitignore and .dockerignore. The exported credentials file lives in the workspace during the job (it's auto-removed at job end), so anything that packages the workspace — a Docker build, a release artifact — can accidentally bundle it1.

The demo image us-docker.pkg.dev/cloudrun/container/hello:latest is Google's sample container, referenced exactly as the official deploy-cloudrun README does2; in a real pipeline you'd substitute your own pinned image tag.

Commit, push to main, and watch the run. One operational note: pool, provider, and IAM changes are eventually consistent and can take up to 5 minutes to propagate — if the very first run fails with a permission error, wait five minutes and re-run before changing anything1.

Verification

The new Cloud Run service is private by default — new deployments are automatically private, and Cloud Run's product recommendation is that CI/CD systems not set or change the unauthenticated-invocations setting2. Verify it from your own machine with an identity token (your gcloud account needs run.routes.invoke, which Cloud Run Admin includes)8:

curl -s -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  "$(gcloud run services describe hello-keyless \
      --project="${PROJECT_ID}" \
      --region='us-central1' \
      --format='value(status.url)')"

Expected: the sample app's landing page (HTTP 200). And the proof of the whole exercise — check the repo's Settings → Secrets: there is no Google Cloud credential stored anywhere.

Harden for June 18, 2026: immutable subject claims

GitHub announced immutable subject claims for Actions OIDC tokens on April 23, 2026: instead of repo:octocat/my-repo:ref:refs/heads/main, the sub claim gains immutable numeric IDs, e.g. repo:octocat-123456/my-repo-456789:ref:refs/heads/main. Repositories created after June 18, 2026 use the new format automatically, as do existing repositories after a rename or transfer; everything else keeps the old format unless you opt in via repository or organization OIDC settings3.

This closes a real attack: if an org or repo name is deleted and re-registered, the new owner could previously mint tokens with the same sub and inherit your cloud trust.

Our setup is largely insulated because the IAM binding matches on attribute.repository (which stays owner/repo) and the attribute condition matches repository_owner — neither embeds the sub format. But if you tighten google.subject-based bindings (e.g., principal://...subject/repo:org/repo:ref:refs/heads/main for branch pinning), a repo created or renamed after June 18 will carry the ID-suffixed owner/repo in sub and stop matching the legacy pattern. For branch-level restrictions, prefer mapping and asserting on assertion.ref plus assertion.repository in the attribute condition rather than string-matching the whole sub.

Troubleshooting

All of these are documented failure modes from the official auth troubleshooting guide7:

  1. Failed to generate Google Cloud federated token — admission into the pool failed. Your attribute condition rejected the token: check the org/repo spelling and remember conditions are case-sensitive on the Google side.
  2. Failed to generate OAuth 2.0 Access Token — impersonation failed. The roles/iam.workloadIdentityUser binding is missing or its principalSet doesn't match: verify the pool ID in the member string uses the project number and the repo is owner/name.
  3. Permission denied with a pool path in workload_identity_provider — you passed the pool name instead of the full provider name, or used the project ID where the project number is required.
  4. The size of mapped attribute exceeds the 127 bytes limit — your google.subject (repo + branch) is too long. This is a Google Cloud IAM limit; shorten the branch or repo name.
  5. The issuer in ID Token ... does not match the expected ones — you're on GitHub Enterprise Server or a GHEC unique token URL; the issuer isn't https://token.actions.githubusercontent.com. Use your installation's token URL as --issuer-uri.

Also note an org-policy gotcha: if your Google Cloud organization restricts external identity providers, https://token.actions.githubusercontent.com must be allowlisted in the constraints/iam.workloadIdentityPoolProviders constraint before Step 3 will succeed7. And if a later step shells out to gsutil, it won't see these credentials — use gcloud storage instead1.

Next steps

Footnotes

  1. google-github-actions/auth README and inputs reference — https://github.com/google-github-actions/auth 2 3 4 5 6 7 8 9 10

  2. google-github-actions/deploy-cloudrun README (usage, inputs, authorization) — https://github.com/google-github-actions/deploy-cloudrun 2 3 4 5

  3. GitHub Changelog (2026-04-23): Immutable subject claims for GitHub Actions OIDC tokens — https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/ 2

  4. GitHub Docs: OpenID Connect reference — https://docs.github.com/actions/reference/openid-connect-reference

  5. Release dates from the GitHub Releases API: auth v3.0.0 (2025-08-28), deploy-cloudrun v3.0.1 (2025-09-03), checkout v6.0.2 (2026-01-09).

  6. Google Cloud IAM docs: Configure Workload Identity Federation with deployment pipelines — https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines

  7. google-github-actions/auth troubleshooting guide — https://github.com/google-github-actions/auth/blob/main/docs/TROUBLESHOOTING.md 2 3 4

  8. Google Cloud Run docs: Authenticate developers — https://docs.cloud.google.com/run/docs/authenticating/developers