Home› Blog› GCP Architecture Series #44 — Workload Identity Federation for OIDC Providers and CI Systems…
GCP Architecture GCP Architecture Series

GCP Architecture Series #44 — Workload Identity Federation for OIDC Providers and CI Systems

#43 federated with AWS, where the provider is your own account and the trust boundary comes for free. GitHub, GitLab and Terraform Cloud share one issuer URL across every customer they have — so configuring the provider and stopping there does not trust your CI system, it trusts all of GitHub.

Verified against current vendor documentation on 26 September 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

#43 federated with AWS, where your account number is the boundary. CI providers do not work that way, and the difference is the whole post.

1
"We configured the GitHub issuer, so only our workflows can authenticate"

No. Some providers, such as GitHub and Terraform Cloud, use a single issuer URL across all of their tenants — the issuer identifies all of GitHub, not a specific GitHub organization. Anybody with a GitHub account gets tokens from https://token.actions.githubusercontent.com/.

Correct approach

The attribute condition is the trust boundary, not the issuer. Google recommends configuring one to check that the token originated from a trusted tenant, and for these providers it is not optional.

2
"We will condition on the organisation name"

That is what the setup guide shows, and it conflicts with the best-practice rule from #43 to use attributes that cannot be reused. A GitHub organisation can be renamed, and the name becomes available again.

Correct approach

Condition on repository_owner_id rather than repository_owner where you can — the numeric ID is the one that cannot be reclaimed by somebody else.

3
"We mapped the subject, that identifies the workflow"

Not reliably. The value for the GitHub Actions OIDC token subject can vary depending on the source event — a push, a pull request and a tag do not produce the same sub.

Correct approach

Map the specific claims you intend to use, and be sure to map any claims that you plan to use as attribute conditions. Do not lean on sub as though it were a stable identifier.

4
"We added a second provider for the new pipeline"

Now one external identity is two IAM principals, and revocation becomes unreliable: an administrator might revoke access for one principal, but might be unaware of the existence of another principal, inadvertently causing the external identity to retain access.

Correct approach

One pool, one provider, and an attribute condition that covers both pipelines. Federating twice is how access survives being revoked.

Architecture

The mechanics are #43's. What changes is where the trust boundary sits, and therefore which field is load-bearing.

Diagram: why a multi-tenant OIDC issuer URL such as GitHub Actions does not identify an organisation, how the attribute condition becomes the trust boundary, which GitHub claims are stable and non-reusable, and three further ways the federation trust can be subverted after configuration
With AWS the account is the boundary. With GitHub the boundary is a CEL expression you have to remember to write.

Where the boundary moved

AWS (#43)GitHub, GitLab, Terraform Cloud
What the issuer identifies Verified against your AWS account via GetCallerIdentity. All of GitHub. One URL for every tenant.
Who can obtain a token from it Principals in your AWS account. Anybody with an account on the platform.
The trust boundary Implicit in the provider configuration. The attribute condition, and nothing else.
Forgetting it Not possible in the same way. Configuration completes successfully.

The last row is why this deserves its own post. A missing attribute condition is not an error, a warning, or a failed setup. The pool works, the pipeline authenticates, the deploy succeeds — and the same door opens for a workflow in a repository you have never heard of.

The claims worth knowing

GitHub Actions tokens carry several claims, and they are not equally trustworthy as identifiers:

  • repository_owner — the username or organisation name, for example google. What the setup guide conditions on.
  • repository_owner_id — the unique owner ID. Not reclaimable.
  • repository — owner and repository name together, for example google/guava.
  • repository_id — contains the unique repository ID.
  • ref — the branch or tag, which lets a condition require refs/heads/main.
  • sub — varies by source event. Useful, but not an identity.
Two Google pages, two different answers

The deployment-pipelines guide gives the attribute condition as assertion.repository_owner=='ORGANIZATION' — the organisation name. The best-practices page, covered in #43, says to use attributes that cannot be reused over time, because a recreated identity sharing an attribute becomes indistinguishable from the original. GitHub organisation names are reusable: rename an organisation and the old name is free for anyone to take. Both pages are correct about their own subject and they point in different directions, and the guide is the one people follow because it is the one with the commands in it. Where the tooling allows, condition on the numeric repository_owner_id — and if you use the name, understand you are trusting that nobody renames the organisation.

Three more ways the trust gets subverted

  1. Federating twice. Two providers for the same IdP means one identity maps to two principals. Revoke one and the other survives — the failure is silent and looks exactly like a completed revocation.
  2. JWKS swapping. If your IdP sits behind a load balancer or reverse proxy that terminates TLS, the TLS connection ensures the authenticity of the load balancer or reverse proxy, but not of the actual identity provider. Swapping the JWKS lets a bad actor sign tokens that are considered valid by Workload Identity Federation.
  3. A credential configuration you were handed. These files contain URLs and paths. You must validate the JSON before using it, or a malicious actor could use the credential configuration to cause your workload to access malicious endpoints.

Why This Architecture Holds Up

Every misconfiguration in this block so far has had a tell. A missing expect: fails a build. An unsupported permission is rejected. An over-long JWT is refused by its recipient. The multi-tenant issuer has no tell at all, and that is worth dwelling on.

Configure a GitHub provider with no attribute condition and every single thing you test will pass. Your workflow authenticates. Your deploy runs. The audit log shows your pipeline doing exactly what it should. Nothing in Google Cloud, GitHub or your pipeline output indicates that the pool will accept a token from any GitHub account in the world, because from the pool's perspective nothing is wrong — it was told to trust that issuer and it does.

The gap only becomes visible if somebody asks the right question, and the right question is unnatural: not "does our pipeline work" but "whose tokens does this issuer sign". For AWS in #43 that question answers itself, because the issuer is your account. Here the answer is "everyone's", and the configuration screen does not say so.

That makes this the one place in the federation design where the documentation's ordering works against you. The attribute condition appears as an optional field, described as a way to restrict access — language that suggests narrowing something already bounded. For a multi-tenant issuer it is not narrowing anything. It is the only boundary there is.

What to do with this

  1. Treat the attribute condition as required for any shared-issuer provider. Not a hardening step — the boundary itself.
  2. Condition on IDs, not names, where the claim exists. Names can be released and reclaimed.
  3. Add the branch when it matters. assertion.ref=='refs/heads/main' stops a fork or a feature branch minting deploy credentials.
  4. Never federate the same provider twice. One pool, one provider, one principal per identity — otherwise revocation is a guess.
  5. Audit existing pools for missing conditions first. This is the configuration most likely to be wrong and least likely to have complained.
  6. Validate any credential configuration you did not write before a workload loads it.

Key Architecture Decisions

DecisionChoose thisBecause
Federating with GitHub or Terraform Cloud An attribute condition, always One issuer URL covers all of their tenants.
What to condition on The numeric owner or repository ID Names can be renamed and reclaimed; IDs cannot.
Restricting to a branch Add assertion.ref It limits which workflows can mint credentials.
Using sub as the identity No Its value varies depending on the source event.
Claims you plan to condition on Map them explicitly Be sure to map any claims you plan to use as conditions.
A second pipeline on the same IdP Extend the condition, not a new provider Two principals make revocation unreliable.
A self-hosted IdP behind a proxy Verify the IdP, not the proxy TLS authenticates the load balancer, not the provider.
A credential configuration from elsewhere Validate the JSON first It can point a workload at malicious endpoints.
Azure DevOps audience fb60f99c-7a34-4190-8149-302f77469936 It is the application ID of the Azure Token Exchange Endpoint.

Closing Thought

The interesting thing about federating with a CI system is how much of the security depends on one optional-looking field. Everything else in the setup is mechanical: an issuer URL you copy, an audience you accept, a mapping you adapt from an example. The attribute condition is the only part that requires you to have understood what you are doing, and it is the only part that will not complain if you skip it.

That asymmetry is worth naming because it generalises. A shared-tenancy identity provider hands you a token that proves a true thing — this came from GitHub — which is not the thing you wanted proved. The gap between those two statements is invisible in every test you would think to run, and it is closed by one CEL expression that nobody reviews after the pipeline goes green. If you inherit a federation configuration from anyone, that expression is the first thing to read, and an empty one is not a simpler configuration. It is a different one.

Next in this series

#45 brings federation inside Google Cloud: Workload Identity for GKE — how a Kubernetes service account becomes a Google Cloud principal, and why the cluster is the trust boundary that AWS and GitHub had to be given explicitly.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent