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.
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/.
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.
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.
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.
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.
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 approachOne 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.
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 examplegoogle. What the setup guide conditions on.repository_owner_id— the unique owner ID. Not reclaimable.repository— owner and repository name together, for examplegoogle/guava.repository_id— contains the unique repository ID.ref— the branch or tag, which lets a condition requirerefs/heads/main.sub— varies by source event. Useful, but not an identity.
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
- 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.
- 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.
- 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
- Treat the attribute condition as required for any shared-issuer provider. Not a hardening step — the boundary itself.
- Condition on IDs, not names, where the claim exists. Names can be released and reclaimed.
- Add the branch when it matters.
assertion.ref=='refs/heads/main'stops a fork or a feature branch minting deploy credentials. - Never federate the same provider twice. One pool, one provider, one principal per identity — otherwise revocation is a guess.
- Audit existing pools for missing conditions first. This is the configuration most likely to be wrong and least likely to have complained.
- Validate any credential configuration you did not write before a workload loads it.
Key Architecture Decisions
| Decision | Choose this | Because |
|---|---|---|
| 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.
#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