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
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
| Side | Holds |
|---|---|
| GitHub | nothing of yours. It mints a token about itself |
| the provider | the trust: issuer, mapping, condition |
| the binding | the authorisation: either on the resource, or on a service account |
| anywhere | no 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.
| Result | Meaning |
|---|---|
401 authError | the exchange failed — issuer, mapping, or id-token: write |
403 forbidden | the exchange worked; the binding is wrong. A much better failure |
| 200 | done. 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.