
The standard Workload Identity Federation tutorial trusts "any workflow in repo X" — which quietly means every workflow file, every branch, and every pull request from a stranger's fork all get a path to the same production credentials. The interesting engineering isn't wiring up WIF at all; it's narrowing that trust down to exactly the one workflow file, on exactly one branch, that should ever be allowed to become that identity — and then proving, live, that everything else genuinely gets refused.
gcp-keyless-cicd-supply-chain ships a release pipeline with no stored Google credential anywhere: GitHub's own OIDC token exchanges for a short-lived Google access token, gated first by the repository's immutable numeric id and owner, then by the exact workflow file (job_workflow_ref) on main. Every image is signed keyless with Sigstore, carries an SBOM and SLSA build provenance as signed attestations, and the deploy workflow verifies all three before it will ship a digest. It deployed live in europe-west1, repository soodrajesh/gcp-keyless-cicd-supply-chain, and the test suite passed 42 of 42 — including four separate live drills designed to try to break the boundary, not just exercise the happy path.
Two gates, and the second one is the workflow file itself
Gate one is the provider's attribute condition: it accepts a token only when repository_id and repository_owner_id match — numeric ids, not names, so a repository that's renamed or deleted-and-recreated can't inherit the trust. Gate two is per service account: the publisher's workloadIdentityUser binding names exactly one principal, release.yml@refs/heads/main; the deployer's names exactly one too, deploy.yml@refs/heads/main. job_workflow_ref is specifically the file that contains the running job — for a reusable workflow, that's the called file — so release.yml calling deploy.yml as a reusable workflow means the deploy job's own token correctly carries deploy.yml's identity, not release.yml's.
Four drills, not one happy path
A pipeline that only demonstrates a successful release proves almost nothing about the boundary. This one runs four adversarial checks against the live deployment: a workflow file that exists in the repository but isn't in either IAM binding tries to impersonate both service accounts and is refused for both (with the real IAM denial captured in the log, not just a green checkmark taken on faith); the deploy workflow dispatched from a branch instead of main fails at the authentication step itself, before it ever reaches signature verification; an unsigned image pushed straight from a laptop is refused at "Verify signature," with production left serving whatever it served before; and an image validly signed via real Sigstore — but by a different, unrelated service identity than the pipeline's own — is also refused, because verification pins the exact certificate identity and issuer, not "any valid signature."
RESULT unsigned conclusion=failure failed_step=Verify signature
RESULT other conclusion=failure failed_step=Verify signature (signed by a real, different identity)
RESULT genuine conclusion=success (the pipeline's own image, same workflow)
Six decisions, honestly framed
| Decision | Trade-off |
|---|---|
| Sigstore's public-good instance, not Binary Authorization with KMS keys | No key management at all, but enforcement lives in the workflow rather than the platform — see below |
| Deploy by digest; tags immutable | A tag can never be silently repointed, at the cost of needing a fresh unique tag per run |
| Pull requests get zero cloud credentials, even from forks | ci.yml lints, tests and scans, but genuinely cannot obtain a Google token |
| Every third-party Action pinned to a full commit SHA | Enforced in CI by a one-line pin checker, not just a convention |
The digest input is validated against ^sha256:[0-9a-f]{64}$ and passed via env | Never interpolated into a shell string, closing an obvious injection path |
| The rogue-signer drill signs as a different service account, not a human | A cleaner negative control than reusing the operator's own identity for "someone else" |
The stated gap
Enforcement here lives in the deploy workflow, not the platform: a human or service holding roles/run.developer on the Cloud Run service directly could still deploy an unsigned image without going through deploy.yml at all. In this build that role is scoped to the deployer service account (itself only impersonable by deploy.yml on main) and the project owner — but closing the gap fully needs Binary Authorization with an attestor that trusts this Sigstore identity, turning "the pipeline refuses rogue images" into "the platform refuses them no matter how someone tries." That's exactly the model the security posture scanner would then be able to verify is actually configured project-wide.
Two bugs a live rebuild found
The Workload Identity pool's display name — built as GitHub Actions (owner/repo) — exceeded Google's 32-character limit on the very first apply, a validation the API only surfaces at creation time. And the negative-control workflow initially ran google-github-actions/auth without token_format: access_token, which only writes a local credentials file and never actually calls IAM — so the drill reported "success" while proving nothing about authorization. Forcing the real token exchange turned a green-but-meaningless check into a genuine one that now fails exactly when it should.
What this doesn't claim to be
A compromised main branch can still change release.yml itself — branch protection is a repository setting this build can't prove from the outside, and it's the real control here. Gate one (the repository id check) is asserted by reading the provider's condition, not attacked with a genuine second repository, since this token lacks safe permission to create and delete one. And because Sigstore's public-good instance is used, the repository and its signing metadata are public — don't lift this exact setup into a private repository without a private Sigstore deployment.
Try it yourself
git clone https://github.com/soodrajesh/gcp-keyless-cicd-supply-chain
cd gcp-keyless-cicd-supply-chain
gcloud config set project <your-project> # push this repo to your own GitHub first
./scripts/up.sh # infra → the REAL release pipeline runs on GitHub Actions → live tests + 4 drills
./scripts/down.sh # delete everything, including the GitHub repository variables
Free-tier: Cloud Run scales to zero, Artifact Registry storage is small, and Workload Identity and Sigstore cost nothing to use.
📢 Have questions or feedback? Drop a comment below or connect with me on Twitter/X@spysood!