id-token: write steps, but the token they receive is minted by Preloop: a different issuer and a different signing key. Each cloud must be told to trust that issuer before the first deploy job runs. preloop ci oidc check proves the trust exists — it does not infer it from configuration.
What the check does
For a repository, ref, and optional environment, the control plane:-
Mints an OIDC token with the claims a real job on that repository would receive:
iss,sub,aud,repository,repository_owner,ref,event_name,environment,org_id, and the immutable-subject form when the repository has one. -
Spends the token once against the provider’s token exchange:
-
Reports
trusted,rejected, orerror, with the provider’s sanitized reason. The token and any credentials the provider returned are discarded immediately and never appear in the response.
error, not a rejected: the trust may be fine, but the control plane cannot reach the server. Publish Vault on an address the control plane can route to, or run the check from a network that can.
:::
Run the check
.github/workflows, takes every cloud trust relationship it finds together with any literal provider inputs (role-to-assume, workload_identity_provider, client-id, url, …), and checks each provider:
The command exits non-zero when any provider is not
trusted, so it can gate a migration pipeline. Checks are rate-limited per repository (five per minute per control-plane node).
Trust condition
Always condition on the fullsub. A policy that matches only repo:OWNER/REPO:* trusts a repository name, and a name is only meaningful when the tenant that minted the token provably owns it. Add org_id only where the provider supports claim conditions: GCP attribute conditions and Vault bound_claims accept it; Azure and AWS cannot use it (see each section), so their tenant binding comes from the per-org issuer path or an exact subject.
AWS
sts:AssumeRoleWithWebIdentity and conditions keyed by the issuer host:
StringLike with repo:acme/api:ref:refs/heads/* only when every branch of the repository should deploy. A custom IAM OIDC provider exposes only sub, aud, amr and oaud as condition keys; a …:org_id condition makes every AssumeRoleWithWebIdentity deny. org_id belongs in Preloop’s Store/ABAC decisions, not in an IAM trust policy.
Google Cloud
The workload identity provider must use Preloop as its issuer, allow the provider resource as an audience, and mapassertion.sub and assertion.org_id:
--member "principal://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/preloop/subject/…" with the exact subject, or an attribute.org_id principalSet.
Azure
org_id. Keep the subject exact. The hosted issuer path is per organization (https://oidc.preloop.dev/o/<org-id>), so this restriction stays tenant-safe there; a single-tenant deployment that shares an issuer between tenants must not rely on subject alone.
HashiCorp Vault
bound_audiences must equal the audience the token carries. When the workflow’s hashicorp/vault-action step does not set jwtGithubAudience, Preloop uses the GitHub-style default https://github.com/<owner>; pass --audience to check another value.
Configuration
[oidc] organization binds the control plane’s tenant into every token as org_id:
org_id from the authenticated organization, not from this node-wide setting. Until it does, a trust policy that conditions on sub alone cannot distinguish two tenants whose submitted repository names collide.