Pattern — GitHub Actions deploys to Google Cloud with no key anywhere

The end-to-end handoff: GitHub mints an OIDC token, a workload identity pool provider trusts it under an attribute condition, and a binding grants the deploy its permissions. With the known-bad configurations that make this pattern dangerous instead of safe.

markdownpatternv1Q3VX-BKMNstage: deploytrust: officialfamily: identityservice: iaminterface: clikeyless CIgithub actionsworkload identity federationattribute conditionprincipalSetid-token: writeknown-bad configurationroles/iam.workloadIdentityUserdeploy pipeline

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

GitHub Actions → Google Cloud, keyless

The handoff

GitHub Actions workflow
   │  mints an OIDC token  (permissions: id-token: write)
   ▼
workload identity pool PROVIDER
   │  issuer-uri   = GitHub's Actions issuer
   │  mapping      = google.subject=assertion.sub
   │  condition    = assertion.repository_owner=='ORGANIZATION'   ← the security boundary
   ▼
Security Token Service  →  a federated access token
   │
   ├── direct resource access  →  the resource's own allow policy   (recommended)
   └── impersonation           →  a service account, via roles/iam.workloadIdentityUser
   ▼
the deploy

Build order and every command: guides/identity/github-actions-without-keys.md.

Who holds what

SideHolds
GitHubnothing of yours. It mints a token about itself
the providerthe trust: issuer, mapping, condition
the bindingthe authorisation: either on the resource, or on a service account
anywhereno key. If a step needs one, the step is wrong

Known-bad configurations

These are the configurations that make this pattern worse than a key, and each one is a real review finding.

1. A provider with no attribute condition

--issuer-uri=<the GitHub Actions issuer URI>      # see the guide for the literal value
--attribute-mapping=<google.subject from assertion.sub>
# --attribute-condition   <-- ABSENT

Why it is bad: that issuer is shared by every workflow on GitHub, not only yours. A provider that trusts the issuer with no condition trusts all of them. The recommended condition — assertion.repository_owner=='ORGANIZATION' — is the only thing narrowing it.

2. A principalSet:// binding broader than intended

principal:// names one external identity. principalSet:// names every identity whose mapped attribute has a value. A set keyed on the organization grants every repository in it; a set keyed on the repository grants every branch and every workflow in that repository.

Why it is bad: it is invisible in review. Both forms are one line and look alike. Decide the blast radius deliberately, and prefer the narrowest that still works.

3. The project id where the project number belongs

principal://iam.googleapis.com/projects/PROJECT_NUMBER/...     # correct
principal://iam.googleapis.com/projects/my-project-id/...      # accepted, never matches

Why it is bad: it fails open-looking — the binding exists, the deploy is denied, and the instinct is to widen something. Nothing was ever matched.

4. Impersonation chosen by default when direct resource access would do

Direct resource access is the recommended path and removes one object per workload. Impersonation is correct only when something downstream requires a service account.

Why it matters: every service account is another object with another policy and another audit surface.

5. A key reintroduced "temporarily" because an API did not work

The vendor states: *"Although most Google Cloud APIs support Workload Identity Federation, some APIs have limitations."* Meeting one of those is not a reason to create a key; it is a reason to find which API and design around it.

Why it is bad: the temporary key is the permanent key. This is the single most common way a keyless design degrades.

Verification

The first call the workflow makes is the test.

ResultMeaning
401 authErrorthe exchange failed — issuer, mapping, or id-token: write
403 forbiddenthe exchange worked; the binding is wrong. A much better failure
200done. Now confirm no key exists in the repository, the runner, or any secret store

And the negative test that proves the boundary: a workflow in another organization, or another repository outside the condition, must fail. A pattern verified only by its success case is not verified.

Read this document as markdown — the same bytes an agent receives.