{"id":165740,"date":"2026-04-11T09:52:35","date_gmt":"2026-04-11T06:52:35","guid":{"rendered":"https:\/\/computingforgeeks.com\/gcp-workload-identity-federation-github-actions\/"},"modified":"2026-04-11T09:52:35","modified_gmt":"2026-04-11T06:52:35","slug":"gcp-workload-identity-federation-github-actions","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/gcp-workload-identity-federation-github-actions\/","title":{"rendered":"Set Up GCP Workload Identity Federation for GitHub Actions (2026)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A GitHub Actions workflow that deploys to GCP needs some way to authenticate. For years the answer was a JSON service account key stored as a GitHub repository secret. Everyone knew it was the wrong answer, everyone used it anyway, and the industry produced an endless supply of blog posts about accidentally leaked keys. Workload Identity Federation is the fix. Instead of a static key, GitHub&#8217;s runner exchanges its built-in OIDC token for a short-lived GCP access token through a trust relationship you configure once. The key file goes away. The rotation problem goes away. The &#8220;we left a demo secret in a public repo&#8221; incident goes away. This guide walks through the exact setup on a real project, the IAM policy binding that actually scopes access to one repository, the GitHub workflow YAML that authenticates and then pushes to Artifact Registry, and the four errors you will hit on the first attempt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the external Workload Identity Federation product, not to be confused with <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\">Workload Identity Federation for GKE<\/a>. They share the underlying Security Token Service but they are configured differently: GKE&#8217;s pool is managed automatically for Kubernetes ServiceAccounts, while the external WIF product is what you use for GitHub Actions, AWS EC2 instances, Azure VMs, on-prem OIDC workloads, and anything else that lives outside a GKE cluster. This guide is the external WIF version specifically for GitHub Actions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Tested April 2026 on GCP with google-cloud-cli 521, GitHub Actions runner ubuntu-24.04, and the google-github-actions\/auth v3 action<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Workload Identity Federation Matters<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The old way was a JSON service account key, base64-encoded, stashed in a GitHub secret, pulled into the runner, written to a file, and passed to <code>gcloud auth activate-service-account<\/code>. Every step in that chain is a place where the key can leak. The workflow author can accidentally print it in a log. A malicious action can read the file. A forked PR workflow can exfiltrate it. The key itself lives forever unless somebody rotates it manually, and nobody rotates it manually.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Workload Identity Federation flips the model. GitHub already issues a signed OIDC token to every running job (<code>$ACTIONS_ID_TOKEN_REQUEST_URL<\/code> and <code>$ACTIONS_ID_TOKEN_REQUEST_TOKEN<\/code>). That token is short-lived, tied to the specific job run, and contains claims about which repository, branch, environment, and actor triggered it. WIF configures GCP&#8217;s Security Token Service to accept that token as proof of identity, exchange it for a federated Google token, and optionally chain-impersonate a regular service account. The resulting access token expires after an hour. No key file ever touches the runner. If an attacker somehow leaks the access token, the blast radius is one hour on the specific resources that one service account was granted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Google officially deprecated service account key files for CI\/CD in 2023 and the organization policy that blocks new key creation is the default in new organizations. If you are still using JSON keys for GitHub Actions in 2026, you are on borrowed time. Migrate before the next org-policy refresh forces the issue.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Mental Model: Four Pieces That Click Together<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WIF has four configuration objects and understanding how they chain together is the hard part. Once the model clicks, the commands are mechanical.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Workload Identity Pool<\/strong>. A container for external identities inside a GCP project. You create one pool per external identity provider family (one for GitHub, one for GitLab, one for AWS, and so on). The pool is where IAM bindings are anchored.<\/li>\n<li><strong>Workload Identity Pool Provider<\/strong>. An OIDC (or SAML or AWS) configuration attached to the pool. For GitHub Actions this is an OIDC provider pointing at <code>https:\/\/token.actions.githubusercontent.com<\/code>. The provider defines which claims from the incoming token get mapped to GCP attributes (repo name, actor, branch, etc.) and an optional attribute condition that filters which incoming tokens are even considered.<\/li>\n<li><strong>Principal<\/strong>. A reference to an external identity that GCP IAM can grant roles to. For GitHub Actions the principal is usually a <code>principalSet:\/\/<\/code> URL that matches every token from a specific repository, or a <code>principal:\/\/<\/code> URL that matches one specific token (subject claim).<\/li>\n<li><strong>Google Service Account<\/strong> (optional chain-impersonation). Two patterns are supported. Direct resource access binds the GitHub principal directly to an IAM role on a GCP resource. Chain-impersonation binds the GitHub principal to <code>roles\/iam.workloadIdentityUser<\/code> on a normal Google Service Account, and then the workflow impersonates that GSA to get an access token. Both work. Direct is newer and simpler, impersonation is the default in most tutorials because it works with every API.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The chain reads: GitHub job produces OIDC token \u2192 provider validates the token and maps claims to attributes \u2192 IAM policy on the target (either the service account or the resource directly) matches the principal \u2192 GCP issues a short-lived access token.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A GCP project with billing enabled<\/li>\n<li><code>gcloud<\/code> CLI 520+ authenticated as an owner or security admin<\/li>\n<li><code>iam.googleapis.com<\/code>, <code>iamcredentials.googleapis.com<\/code>, and whatever APIs your workflow will call enabled. For this guide&#8217;s example, also <code>artifactregistry.googleapis.com<\/code><\/li>\n<li>A GitHub repository you control where you can add a workflow file<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Enable the APIs:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud services enable \\\n  iam.googleapis.com \\\n  iamcredentials.googleapis.com \\\n  artifactregistry.googleapis.com \\\n  --project=PROJECT_ID<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Create the Workload Identity Pool<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Capture the project ID and project number first. Both are needed at different points in the setup and mixing them up is the single most common mistake:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>PROJECT_ID=$(gcloud config get-value project)\nPROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format=\"value(projectNumber)\")\necho \"ID: $PROJECT_ID\"\necho \"NUMBER: $PROJECT_NUMBER\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create a pool. One pool per external identity family is the recommended shape, so name it something like <code>github-pool<\/code> or <code>github-pool-prod<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud iam workload-identity-pools create github-pool \\\n  --location=global \\\n  --display-name=\"GitHub Actions pool\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Output confirms the pool was created:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Created workload identity pool [github-pool].<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Create the GitHub OIDC Provider<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The provider is where you define how GCP validates the incoming GitHub token. Three things matter here. The <code>issuer-uri<\/code> is the fixed GitHub Actions OIDC endpoint. The <code>attribute-mapping<\/code> translates claims from the GitHub token into GCP attributes you can reference in IAM bindings. The <code>attribute-condition<\/code> is a CEL expression that filters which tokens are even considered, and this is the single most important security control: it is how you prevent any GitHub repository on the internet from assuming your identity.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud iam workload-identity-pools providers create-oidc github-provider \\\n  --location=global \\\n  --workload-identity-pool=github-pool \\\n  --display-name=\"GitHub OIDC provider\" \\\n  --issuer-uri=\"https:\/\/token.actions.githubusercontent.com\" \\\n  --attribute-mapping=\"google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner\" \\\n  --attribute-condition=\"assertion.repository_owner == 'your-org-name'\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>your-org-name<\/code> with your actual GitHub organization or username. The attribute condition is non-optional. Without it, any GitHub repository on the internet could potentially trigger the exchange, and the Google Cloud docs now refuse to create a provider that has no condition at all. Output confirms creation:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>Created workload identity pool provider [github-provider].<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Capture the full provider resource name. This is what the GitHub workflow YAML will reference. The format is <code>projects\/PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/POOL\/providers\/PROVIDER<\/code> and that exact string goes into the workflow:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud iam workload-identity-pools providers describe github-provider \\\n  --location=global \\\n  --workload-identity-pool=github-pool \\\n  --format=\"value(name)\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code code\"><code>projects\/PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/github-pool\/providers\/github-provider<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Create a Service Account for the Workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Chain impersonation is the pattern most tutorials show. You create a normal Google Service Account, grant it the IAM roles the workflow actually needs (Artifact Registry writer, Cloud Run deployer, whatever), and then allow the GitHub principal to impersonate it. The workflow does not hold any credentials for this SA; it fetches a short-lived access token through the impersonation chain.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud iam service-accounts create github-deploy \\\n  --display-name=\"GitHub Actions deployer\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Grant the SA the roles the workflow actually needs. For this guide&#8217;s Artifact Registry push example, that is <code>artifactregistry.writer<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud projects add-iam-policy-binding $PROJECT_ID \\\n  --role=roles\/artifactregistry.writer \\\n  --member=\"serviceAccount:github-deploy@$PROJECT_ID.iam.gserviceaccount.com\" \\\n  --condition=None<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the workflow will deploy to Cloud Run, grant <code>run.developer<\/code> and <code>iam.serviceAccountUser<\/code> on the Cloud Run runtime SA as well. The rule of thumb is to grant only what the workflow actually calls; resist the temptation to grant <code>editor<\/code> or <code>owner<\/code> because it is easier.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Bind the GitHub Principal to the Service Account<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the step that ties the GitHub repository to the GCP identity. The <code>principalSet<\/code> URL matches every OIDC token from a specific repository, because the attribute mapping you configured earlier exposed <code>attribute.repository<\/code>. The format uses the pool resource name and the attribute name:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud iam service-accounts add-iam-policy-binding \\\n  github-deploy@$PROJECT_ID.iam.gserviceaccount.com \\\n  --role=roles\/iam.workloadIdentityUser \\\n  --member=\"principalSet:\/\/iam.googleapis.com\/projects\/$PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/github-pool\/attribute.repository\/your-org-name\/your-repo-name\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>your-org-name\/your-repo-name<\/code> with the actual GitHub <code>owner\/repo<\/code> string. This binding means: any token from that repository can impersonate the <code>github-deploy<\/code> service account, provided it also satisfies the attribute condition on the provider. If you want to restrict impersonation to a specific branch, use <code>attribute.ref<\/code> instead and match <code>refs\/heads\/main<\/code>. If you want to restrict to a GitHub Environment (for production promotion approvals), use <code>attribute.environment<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Output confirms the binding:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>- members:\n  - principalSet:\/\/iam.googleapis.com\/projects\/PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/github-pool\/attribute.repository\/your-org-name\/your-repo-name\n  role: roles\/iam.workloadIdentityUser<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: The GitHub Actions Workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow uses Google&#8217;s official <code>google-github-actions\/auth<\/code> action, currently on v3. Two things are non-negotiable: the <code>id-token: write<\/code> permission on the job, and the <code>workload_identity_provider<\/code> input set to the full provider resource name captured earlier.<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>vim .github\/workflows\/deploy.yml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow authenticates via WIF, installs gcloud, configures Docker to push to Artifact Registry, builds an image, and pushes it. Every step runs with the short-lived federated token, no JSON key file anywhere:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>name: Deploy to GCP\n\non:\n  push:\n    branches: [main]\n\njobs:\n  push-image:\n    runs-on: ubuntu-24.04\n    permissions:\n      contents: read\n      id-token: write\n\n    steps:\n    - uses: actions\/checkout@v4\n\n    - name: Authenticate to Google Cloud\n      id: auth\n      uses: google-github-actions\/auth@v3\n      with:\n        workload_identity_provider: 'projects\/PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/github-pool\/providers\/github-provider'\n        service_account: 'github-deploy@PROJECT_ID.iam.gserviceaccount.com'\n\n    - name: Set up gcloud\n      uses: google-github-actions\/setup-gcloud@v2\n\n    - name: Configure Docker for Artifact Registry\n      run: gcloud auth configure-docker europe-west1-docker.pkg.dev\n\n    - name: Build and push image\n      run: |\n        IMAGE=europe-west1-docker.pkg.dev\/PROJECT_ID\/my-repo\/my-app:${{ github.sha }}\n        docker build -t $IMAGE .\n        docker push $IMAGE<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>PROJECT_NUMBER<\/code> and <code>PROJECT_ID<\/code> with your real values. Commit the workflow, push to the branch, and watch it run. On the first run you will almost certainly hit one of the errors documented below because the setup has several moving parts and any of them can be off by one character.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Direct Resource Access Instead of Service Account Impersonation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Service account impersonation is the default, but the newer and slightly simpler pattern is direct resource access. You bind the GitHub principal directly to an IAM role on a GCP resource, skipping the intermediate service account. Less to configure, one fewer thing to audit. The tradeoff is that direct access only works for resources whose IAM policies support the <code>principalSet<\/code> member format, which is most modern APIs but not all of them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same Artifact Registry push without the intermediate SA:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud projects add-iam-policy-binding $PROJECT_ID \\\n  --role=roles\/artifactregistry.writer \\\n  --member=\"principalSet:\/\/iam.googleapis.com\/projects\/$PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/github-pool\/attribute.repository\/your-org-name\/your-repo-name\" \\\n  --condition=None<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow YAML then omits the <code>service_account<\/code> input on the auth action and calls the API directly with the federated token. Check the <a href=\"https:\/\/github.com\/google-github-actions\/auth#direct-workload-identity-federation\" target=\"_blank\" rel=\"noreferrer noopener\">auth action docs<\/a> for the exact workflow shape. Prefer direct access for any new workflow. Fall back to impersonation only if a specific API rejects the principalSet member format.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Terraform Version<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The full setup in Terraform. This is the version to copy into a platform-team-maintained module. Note that <code>google_iam_workload_identity_pool<\/code> is the correct resource for external WIF scenarios like this one, which is different from the GKE Workload Identity pool that you do NOT declare in Terraform:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_iam_workload_identity_pool\" \"github\" {\n  workload_identity_pool_id = \"github-pool\"\n  display_name              = \"GitHub Actions pool\"\n}\n\nresource \"google_iam_workload_identity_pool_provider\" \"github\" {\n  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id\n  workload_identity_pool_provider_id = \"github-provider\"\n  display_name                       = \"GitHub OIDC provider\"\n\n  attribute_mapping = {\n    \"google.subject\"             = \"assertion.sub\"\n    \"attribute.actor\"            = \"assertion.actor\"\n    \"attribute.repository\"       = \"assertion.repository\"\n    \"attribute.repository_owner\" = \"assertion.repository_owner\"\n  }\n\n  attribute_condition = \"assertion.repository_owner == 'your-org-name'\"\n\n  oidc {\n    issuer_uri = \"https:\/\/token.actions.githubusercontent.com\"\n  }\n}\n\nresource \"google_service_account\" \"github_deploy\" {\n  account_id   = \"github-deploy\"\n  display_name = \"GitHub Actions deployer\"\n}\n\nresource \"google_project_iam_member\" \"artifact_writer\" {\n  project = var.project_id\n  role    = \"roles\/artifactregistry.writer\"\n  member  = \"serviceAccount:${google_service_account.github_deploy.email}\"\n}\n\nresource \"google_service_account_iam_member\" \"github_impersonation\" {\n  service_account_id = google_service_account.github_deploy.name\n  role               = \"roles\/iam.workloadIdentityUser\"\n  member             = \"principalSet:\/\/iam.googleapis.com\/projects\/${data.google_project.current.number}\/locations\/global\/workloadIdentityPools\/${google_iam_workload_identity_pool.github.workload_identity_pool_id}\/attribute.repository\/your-org-name\/your-repo-name\"\n}\n\ndata \"google_project\" \"current\" {}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apply with a normal <code>terraform init &amp;&amp; terraform apply<\/code>. The pool and provider are global resources so the <code>location<\/code> is implicitly <code>global<\/code> in the Terraform provider.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;The caller does not have permission&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Full error from the auth action:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>google-github-actions\/auth failed with: failed to generate Google Cloud federated token for projects\/PROJECT_NUMBER\/locations\/global\/workloadIdentityPools\/github-pool\/providers\/github-provider: {\"error\":\"invalid_grant\",\"error_description\":\"The given OIDC token does not match any allowed audiences.\"}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two common causes. First, the attribute condition on the provider does not match the incoming GitHub token. If the condition requires <code>repository_owner == 'acme'<\/code> and the repo is in a personal account, the token&#8217;s owner claim will be the username, not the org, and the match fails. Widen the condition or move the repo. Second, the workflow is running with an OIDC token whose audience does not match what the auth action expects. Upgrade to <code>google-github-actions\/auth@v3<\/code> which handles the audience automatically, and do not override the <code>audience<\/code> input unless you really know what you are doing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Permission &#8216;iam.serviceAccounts.getAccessToken&#8217; denied&#8221;<\/h3>\n\n\n\n<pre class=\"wp-block-code code\"><code>Error: google-github-actions\/auth failed with: failed to generate Google Cloud access token for github-deploy@PROJECT_ID.iam.gserviceaccount.com: Permission 'iam.serviceAccounts.getAccessToken' denied on resource (or it may not exist).<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The GitHub principal does not have <code>roles\/iam.workloadIdentityUser<\/code> on the target service account, or the principalSet member string has a typo. Recheck the <code>add-iam-policy-binding<\/code> command and make sure the repository path exactly matches the repo the workflow is running in (case-sensitive, no trailing slash). If you recently added the binding, wait two to seven minutes for IAM propagation before retrying. If the binding is there and still failing, double-check that the provider resource name in the workflow uses the project <strong>number<\/strong>, not the project ID.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Unable to acquire impersonation credentials&#8221;<\/h3>\n\n\n\n<pre class=\"wp-block-code code\"><code>UNAUTHENTICATED: Unable to acquire impersonation credentials: Request had invalid authentication credentials<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>iamcredentials.googleapis.com<\/code> API is not enabled on the project, which means <code>generateAccessToken<\/code> calls fail silently with an auth error. Enable the API explicitly and wait thirty seconds for the activation to propagate:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud services enable iamcredentials.googleapis.com<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;The request was missing required parameter &#8216;audience'&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This appears if you set the <code>audience<\/code> input on the auth action incorrectly, usually copied from an older tutorial. The <code>google-github-actions\/auth@v3<\/code> action computes the correct audience automatically from the provider name. Remove any explicit <code>audience<\/code> input from the workflow and let the action figure it out.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;id-token: write&#8221; permission error<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The auth action fails immediately with an error about not being able to fetch the ID token. The job in your workflow YAML is missing the <code>permissions<\/code> block that grants <code>id-token: write<\/code>. GitHub Actions disables OIDC token minting by default and requires the workflow to opt in explicitly. Add the block to the job (not the workflow top level, although top-level works too):<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>jobs:\n  push-image:\n    permissions:\n      contents: read\n      id-token: write<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Scoping Beyond Repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Binding to <code>attribute.repository<\/code> gives every workflow in the repo access. For production setups you usually want tighter scoping. Four common refinements.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Branch-based<\/strong>. Use <code>attribute.ref<\/code> and match <code>refs\/heads\/main<\/code>. Only workflows running on <code>main<\/code> can impersonate the SA. Matches most &#8220;deploy on merge to main&#8221; patterns.<\/li>\n<li><strong>Environment-based<\/strong>. Use <code>attribute.environment<\/code> and match a GitHub Environment name like <code>production<\/code>. GitHub Environments also support required reviewers, which lets you gate sensitive deploys behind human approval.<\/li>\n<li><strong>Workflow-based<\/strong>. Use <code>attribute.workflow<\/code> to restrict to a specific workflow file. Useful when one repo has multiple workflows and only one should touch production.<\/li>\n<li><strong>Combination with attribute conditions<\/strong>. The provider&#8217;s <code>--attribute-condition<\/code> can also enforce multiple predicates. A realistic production condition might require the repository owner, the repo name, the ref, and the environment to all match.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The exact attribute name for each of these is exposed through the mapping you configured on the provider. If an attribute is not in the mapping, you cannot use it in a binding. Re-run <code>providers update-oidc<\/code> to add new mappings when you need them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cleanup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WIF configuration itself costs nothing. The pool, the provider, the service account, and the IAM bindings are all free. The only cost is whatever the workflow actually calls (Artifact Registry storage, Cloud Run requests, BigQuery queries). If you set this up as a demo and want to tear it all down:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud iam service-accounts delete github-deploy@$PROJECT_ID.iam.gserviceaccount.com --quiet\ngcloud iam workload-identity-pools providers delete github-provider \\\n  --location=global --workload-identity-pool=github-pool --quiet\ngcloud iam workload-identity-pools delete github-pool --location=global --quiet<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Deleted pools and providers enter a &#8220;soft-deleted&#8221; state for thirty days before being permanently removed. You can restore them during that window if you delete by accident. Pool names are not immediately reusable during the soft-delete period, so pick a different name if you need to rebuild quickly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between Workload Identity Federation and Workload Identity Federation for GKE?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Workload Identity Federation is the general-purpose product for external workloads (GitHub Actions, AWS EC2, Azure VMs, on-prem OIDC) authenticating to GCP. You configure the pool and provider manually. Workload Identity Federation for GKE is a specialized version for Kubernetes ServiceAccounts inside a GKE cluster, where GCP manages the pool automatically at <code>PROJECT_ID.svc.id.goog<\/code>. They share the Security Token Service but are configured differently. This article covers the general external WIF product for GitHub Actions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I still need a service account with WIF?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You need either a service account (chain impersonation mode) or direct resource access on the GitHub principal. The SA route is more widely supported because every GCP API honors IAM policies on service accounts. Direct access is newer and simpler but some older APIs do not yet accept <code>principalSet<\/code> member formats in their IAM policies. Start with SA impersonation, move to direct access where it works, skip entirely only if you know every API you call supports it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I restrict access to a specific branch?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bind the principal to <code>attribute.ref<\/code> matching <code>refs\/heads\/main<\/code> instead of the broader <code>attribute.repository<\/code>. Make sure the attribute mapping on the provider exposes <code>attribute.ref=assertion.ref<\/code>, otherwise the binding will not match. For production, combine branch restriction with GitHub Environment protection rules so only approved pushes to <code>main<\/code> can impersonate the deploy SA.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can WIF work with self-hosted GitHub Actions runners?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Self-hosted runners receive the same OIDC token from GitHub as hosted runners, and the auth action works identically. The caveat is that self-hosted runners run in your own infrastructure, so they also have whatever identity that infrastructure has. If a runner VM has a workload identity of its own, a workflow could use either the GitHub WIF path or the VM&#8217;s native identity, which complicates the security model. Use WIF exclusively on self-hosted runners if you want a single consistent identity story.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How does this compare to OIDC with AWS?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Very similar conceptually. On AWS you create an IAM OIDC provider pointing at the GitHub issuer, an IAM role with a trust policy that matches the token subject claim, and the workflow uses <code>aws-actions\/configure-aws-credentials<\/code> to assume the role with web identity. The GCP equivalent has a pool and a provider resource instead of a single OIDC provider, uses attribute mappings and conditions instead of trust policy conditions, and can chain through a service account or go direct. Both achieve the same outcome: short-lived cloud credentials derived from the GitHub OIDC token with no static keys.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What happens when GitHub rotates its OIDC signing keys?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing on your side. GCP fetches the current signing keys from GitHub&#8217;s JWKS endpoint on every token verification and caches them for a short period. When GitHub rotates, GCP picks up the new keys automatically. You do not need to update anything in the provider configuration.<\/p>\n\n\n\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\"@type\":\"Question\",\"name\":\"What is the difference between Workload Identity Federation and Workload Identity Federation for GKE?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"WIF is the general product for external workloads like GitHub Actions authenticating to GCP. WIF for GKE is a specialized version for Kubernetes ServiceAccounts where GCP manages the pool automatically. They share STS but are configured differently.\"}},\n    {\"@type\":\"Question\",\"name\":\"Do I still need a service account with WIF?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes for chain impersonation mode, optional for direct resource access. Start with SA impersonation for broad compatibility, move to direct access where the target API supports principalSet members.\"}},\n    {\"@type\":\"Question\",\"name\":\"How do I restrict access to a specific branch?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Bind the principal to attribute.ref matching refs\/heads\/main instead of attribute.repository. Ensure the attribute mapping on the provider exposes attribute.ref=assertion.ref. Combine with GitHub Environment protection rules for production.\"}},\n    {\"@type\":\"Question\",\"name\":\"Can WIF work with self-hosted GitHub Actions runners?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes. Self-hosted runners receive the same OIDC token from GitHub. The caveat is they also have their own VM-level identity, which can complicate the security model. Use WIF exclusively for consistency.\"}},\n    {\"@type\":\"Question\",\"name\":\"How does WIF for GitHub Actions compare to the AWS OIDC equivalent?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Similar concept. AWS uses an IAM OIDC provider plus an IAM role with a trust policy. GCP uses a pool, provider, and optional service account chain. Both produce short-lived cloud credentials from the GitHub OIDC token with no static keys.\"}},\n    {\"@type\":\"Question\",\"name\":\"What happens when GitHub rotates its OIDC signing keys?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Nothing on your side. GCP fetches signing keys from GitHub's JWKS endpoint and caches them briefly. When GitHub rotates, GCP picks up new keys automatically.\"}}\n  ]\n}\n<\/script>\n\n\n\n<h2 class=\"wp-block-heading\">Where to Go Next<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WIF for GitHub Actions is the entry point to credential-free CI\/CD on GCP. The natural next steps are scoping bindings to specific branches or environments, automating the pool\/provider creation with Terraform so every new repository gets a consistent setup, and applying the same pattern to GitLab CI (<code>https:\/\/gitlab.com<\/code> as issuer), CircleCI, and any other CI system with an OIDC story. On the GCP side, the same federated identity can push to <a href=\"https:\/\/computingforgeeks.com\/google-cloud-secret-manager-tutorial\/\">Secret Manager<\/a>, deploy to Cloud Run or GKE, publish images to Artifact Registry, or run BigQuery jobs. The <a href=\"https:\/\/cloud.google.com\/iam\/docs\/workload-identity-federation\" target=\"_blank\" rel=\"noreferrer noopener\">official Workload Identity Federation docs<\/a>, the <a href=\"https:\/\/github.com\/google-github-actions\/auth\" target=\"_blank\" rel=\"noreferrer noopener\">google-github-actions\/auth README<\/a>, and our <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\">GKE Workload Identity guide<\/a> (for the in-cluster equivalent) are the three references worth bookmarking.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Tested guide to configuring GCP Workload Identity Federation for GitHub Actions without JSON service account keys. Pool, provider, attribute conditions, principalSet bindings, and the 5 errors you will hit.<\/p>\n","protected":false},"author":3,"featured_media":165741,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2680,36939],"tags":[36175],"cfg_series":[39811],"class_list":["post-165740","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cloud","category-gcp","tag-gcp","cfg_series-gcp-platform"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/165740","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=165740"}],"version-history":[{"count":0,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/165740\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/165741"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=165740"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=165740"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=165740"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=165740"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}