---
cuppy_hub: gcp
cuppy_path: patterns/github-actions-to-google-cloud-keyless.md
cuppy_section: docs
cuppy_doc: Q3VX-BKMN
cuppy_doc_uuid: ab85c61d-1ca6-4fa6-80cc-282d8b8b880c
cuppy_permalink: https://cuppy.io/gcp/d/Q3VX-BKMN
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/gcp/docs/patterns/github-actions-to-google-cloud-keyless
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 5faa5d81ba2b0528b932a2a95599be3420d32a8d0aff6899a34933a99bd4bebf
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 91 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/gcp/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
