Variations, inputs, services, and caches in Chainguard Checks
Run a Chainguard Checks check once per variation, pass files between checks, check out another repository, start …
For the complete documentation index, see llms.txt.
A check that must authenticate to Google Cloud or to Chainguard, such as to read a Cloud Storage bucket or to pull private images from cgr.dev, can use a workload identity: short-lived credentials that Chainguard Checks gives the check while it runs. This page explains how workload identities work, how to declare and set one up, how to limit which pull requests get one, and how to revoke access.
A check’s VM holds no long-lived credentials. When a check uses a workload identity, Chainguard Checks issues the check an OpenID Connect (OIDC) token that lasts five minutes, renews the token while the check runs, and keeps the real credentials outside the VM:
| Provider | What the check’s VM holds |
|---|---|
| Google Cloud | GOOGLE_APPLICATION_CREDENTIALS, set to /run/chainguard/identities/<identity>/adc.json, a credential configuration that Google Cloud client libraries use without further setup. The tokens that code in the VM can read are sealed stand-ins. Chainguard Checks swaps in the real tokens only on requests to sts.googleapis.com and other googleapis.com subdomains, such as storage.googleapis.com. |
| Chainguard | Nothing. Chainguard Checks adds a credential to the check’s requests to each Chainguard host the identity lists, so the check doesn’t sign in or need a credential file. |
Code in the check can’t read the real tokens, but it can use them: each request it sends to those hosts carries the identity’s access. If that access lets the code create a credential, such as a Google Cloud service account key, that credential works outside the check until you delete it. Grant each identity only the access its checks need.
What code can use depends on where it runs:
| Where code runs | Credentials it can use |
|---|---|
| A check on a pull request | The identity the check names, if identities.yaml on the pull request’s base branch allows it for that check and branch. If your default branch has a trust section, the pull request must also reach the identity’s ring. |
| A check after a change merges | The identity the check names, if identities.yaml on your default branch allows it for that check and branch. |
A Chainguard Workspaces session, including the checks that run-ci runs in it | The session’s own credentials, as described in Manage Chainguard Workspaces session credentials. Sessions don’t get workload identities, so run-ci skips checks that use one. |
You declare workload identities in .chainguard/ci/identities.yaml, which works with either file layout. The file has the following top-level keys:
| Key | Required | Description |
|---|---|---|
version | Yes | The format version. Must be 1. |
identities | Yes | A map from each identity’s name to its definition. Refer to Identity keys. |
trust | No | Which pull requests get identities, decided by who opened them. Refer to Limit which pull requests get an identity. |
Chainguard Checks refuses keys it doesn’t recognize. To check the file before you merge it, run chainctl checks validate ., as described in Validate your configuration offline.
The following file declares an identity named gcs-read that the integration check in .chainguard/ci.yaml can use on pull requests into main:
version: 1
identities:
gcs-read:
description: Read test fixtures from Cloud Storage.
checks:
- ci.checks.integration
audiences:
- https://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/checks/providers/checks
gcp:
provider: projects/123456789012/locations/global/workloadIdentityPools/checks/providers/checks
audience: https://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/checks/providers/checks
allow:
- run: presubmit
target_ref: refs/heads/mainA check uses an identity by naming it with identity. For a Google Cloud identity, the check’s network.egress must list sts.googleapis.com, where client libraries exchange the identity’s token, and each Google API host the check calls:
version: 1
environment:
packages: [go]
checks:
integration:
cmd: go test -tags=integration ./...
identity: gcs-read
network:
egress:
- proxy.golang.org
- sum.golang.org
- sts.googleapis.com
- storage.googleapis.comMerge an identity’s definition before you use it. Chainguard Checks reads definitions from the base branch, so on the pull request that adds gcs-read, the integration check can’t get the identity and fails.
A check that uses an identity can’t set GOOGLE_APPLICATION_CREDENTIALS in its vars or in its environment’s vars. If it does, the whole run fails.
Each identity has the following keys:
| Key | Required | Description |
|---|---|---|
description | No | A description of the identity. |
checks | Yes | A list of selectors for the checks that can use the identity. |
audiences | Yes | A list of the audiences that the identity’s tokens can carry. It must include gcp.audience and each Chainguard host’s exchange_audience. |
allow | Yes | A list of the runs that can get the identity. |
gcp | No | Google Cloud settings, both required: provider, the workload identity pool provider’s resource name, in the form projects/<project-number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>, and audience, an audience that the provider accepts. |
chainguard | No | A map from each Chainguard host to its settings. Refer to Chainguard hosts. |
ring | With a trust section | Which pull requests get the identity. Refer to Limit which pull requests get an identity. |
An identity needs gcp, chainguard, or both to give a check any credentials. Identity names are 1 to 48 lowercase letters, digits, and -, and start with a letter.
Each checks entry has the form <file>.checks.<check>. <file> is a check file’s name without .yaml, such as go for .chainguard/ci/go.yaml, or ci in the single-file layout. <check> is a check’s name in that file. Either part can be * to match any file or any check, but only as the whole part, so go.checks.test-* is an error. Quote a selector that starts with *, such as "*.checks.lint", because YAML reads a leading * as an alias.
Each allow entry names a kind of run and its branch:
run | Ref key | Runs it allows |
|---|---|---|
presubmit | target_ref, such as refs/heads/main | Pull requests whose base branch is that branch. |
postsubmit | ref, such as refs/heads/main | Runs after a change merges to that branch. Chainguard Checks runs these only on your default branch. |
Each ref must be a full ref that starts with refs/.
Each key under chainguard is a Chainguard host, and its value has the following keys, all required:
| Key | Description |
|---|---|
identity | The ID of an assumable identity in your Chainguard organization that trusts this workload identity’s tokens. Refer to Set up the provider. |
exchange_audience | The audience that the assumable identity accepts. It must also be in the identity’s audiences. |
role | The most access the check gets on the host, from the following table. The assumable identity’s role bindings must also grant that access. |
scopes | A list of group IDs, such as your organization’s ID. Only the assumable identity’s role bindings on these groups, or on the groups that contain them, apply. |
Each host accepts the following roles:
| Host | role | What the check can do |
|---|---|---|
cgr.dev | registry.pull | Pull container images. |
cgr.dev | registry.push | Pull and push container images. |
apk.cgr.dev | apk.pull | Download packages. |
libraries.cgr.dev | libraries.javascript.pull | Download JavaScript libraries from Chainguard Libraries. |
libraries.cgr.dev | libraries.python.pull | Download Python libraries from Chainguard Libraries. |
console-api.enforce.dev | console.viewer | List groups, identities, roles, role bindings, and identity providers in the Chainguard API. |
A check that uses a Chainguard host must list it in network.egress, along with any host that the service redirects downloads to.
A definition in identities.yaml grants no access by itself. The provider must also trust the identity’s tokens and grant them access. Chainguard Checks issues each token with the following claims:
| Claim | Value |
|---|---|
iss | https://issuer.enforce.dev |
sub | chainguard:<repository-id>:<identity>, where <repository-id> is a 32-character hexadecimal ID that Chainguard assigns to your GitHub repository, and <identity> is the identity’s name. |
aud | The identity’s gcp.audience, or a Chainguard host’s exchange_audience. |
ref | For a run after merging, the ref it runs on, such as refs/heads/main. Tokens for pull requests have no ref claim. |
Every Chainguard Checks customer’s tokens come from the same issuer, and any repository can ask for any audience, so the subject is what sets your identities apart from everyone else’s. Match the exact subject on the provider:
https://issuer.enforce.dev and the attribute mapping google.subject=assertion.sub. Grant IAM roles to the principal principal://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<pool>/subject/chainguard:<repository-id>:<identity>, never to the whole pool. To refuse other repositories’ tokens outright, also give the provider an attribute condition such as assertion.sub.startsWith('chainguard:<repository-id>:').exchange_audience, and bind it to a role that grants the access the check needs.chainctl doesn’t show your repository’s ID. During the beta, contact your Chainguard account team to set up workload identities and get the ID, or contact Chainguard if you signed up yourself.
Chainguard Checks reads each part of an identity’s use from a different place:
| Part | Where Chainguard Checks reads it |
|---|---|
The identity’s definition in identities.yaml | For a pull request, its base branch, at the commit GitHub merges the pull request into. For a run after merging, your default branch’s latest commit. |
The trust section and each identity’s ring | Your default branch’s latest commit, for every pull request. |
The check’s identity key, its commands, and its network.egress | The commit Chainguard Checks runs, which for a pull request includes the pull request’s changes. |
A pull request can’t add an identity, or widen which checks, branches, or people get one, until it merges. It can change what a check that already gets an identity runs, and which hosts that check can reach. If identities.yaml is invalid on the base branch or on your default branch, no check gets an identity until you fix it.
Without a trust section on your default branch, a check that requests an identity gets it on every open pull request whose base branch matches the identity’s allow rule, including drafts and pull requests from forks, whoever opened them. Those pull requests’ commands run with the identity’s access. Grant a presubmit identity only what an untrusted pull request may do, such as read-only access, or limit which pull requests get it with a trust section.
To give identities only to pull requests opened by people you trust, add a trust section to .chainguard/ci/identities.yaml on your default branch, and give each identity a ring:
version: 1
trust:
groups:
maintainers:
github:
user: {octocat: 583231}
bots:
github:
app: {dependabot: 49699333}
subrings:
trusted:
authors: [maintainers, bots]
github_token: 1.trusted
identities:
gcs-read:
ring: 1.trusted
# ...the rest of the identity's definitionWith this file, the gcs-read identity goes only to pull requests opened by octocat or by Dependabot, and to runs after merging.
The trust section has the following keys:
| Key | Required | Description |
|---|---|---|
groups | Yes | A map from each group’s name to the GitHub accounts in it. Refer to Groups. |
subrings | Yes | A map from each subring’s name to its authors, a list of groups. A pull request meets a subring when the account that opened it is in one of those groups. |
github_token | Yes | The ring for a read-only GitHub token, in the same form as ring. During the beta, checks don’t get this token, but the key is still required. |
cache | No | Which rings share build caches. Refer to Set up cache pools. |
Chainguard Checks places each run in a ring, based on the trust section on your default branch:
| Ring | Runs in the ring |
|---|---|
2 | Runs after merging, on your default branch. |
1.<subring> | Pull requests opened by an account in one of the subring’s authors groups, such as 1.trusted for pull requests that octocat or Dependabot opens in the earlier example. |
0 | Every other pull request. |
Ring 2 is above every subring, and every subring is above ring 0. The trust section uses rings in the following ways:
ring is the lowest ring that gets the identity. With ring: 1.trusted, pull requests in ring 1.trusted and runs after merging get it. With ring: 2, only runs after merging get it, and with ring: 0, every run does, the same as without a trust section.github_token sets the lowest ring that gets a GitHub token, in the same way.A ring only narrows an identity’s allow rules, so a run still needs a matching allow rule to get the identity.
Only the account that opened the pull request counts. Who pushed its commits, who reviewed it, and whether its branch is in a fork don’t change the result.
Because Chainguard Checks reads the trust section and every ring from your default branch, a pull request can’t change its own trust, even a pull request into another branch. An identity that your default branch’s file doesn’t define gets ring 2, so a pull request into another branch never gets an identity that only that branch defines.
When the file has a trust section, every identity in it needs a ring, and every group must be in at least one subring’s authors. Without a trust section, ring is an error. Group and subring names follow the same rules as identity names.
Each group has exactly one source under github:
| Source | Example | Matches |
|---|---|---|
org | {github: {org: acme}} | Members of the GitHub organization. |
team | {github: {team: acme/maintainers}} | Members of the team, including members of its child teams. |
user | {github: {user: {octocat: 583231}}} | The listed GitHub users. |
app | {github: {app: {dependabot: 49699333}}} | Pull requests that the listed GitHub Apps open. |
To trust specific people, list them in a user group and pin each one’s ID, as in the earlier example. Chainguard Checks reads organization and team members from GitHub through the Chainguard App, and can take up to three minutes to see a change to them.
For user and app, map each login to its numeric GitHub account ID. Chainguard Checks matches the ID, and the login is only a label. To find a user’s ID, run the following command, replacing $LOGIN with the user’s GitHub login:
gh api "/users/$LOGIN" --jq .idAn app opens pull requests as its bot account, <slug>[bot]. To find that account’s ID, replace $SLUG with the app’s slug, such as dependabot:
gh api "/users/$SLUG%5Bbot%5D" --jq .idYou can also list logins without IDs, such as user: [octocat]. Chainguard Checks then trusts whoever holds each login when it decides, so pin IDs instead.
On a pull request that doesn’t reach an identity’s ring, a check that requests the identity fails before its commands run, and Chainguard Checks doesn’t retry it. The check’s result on GitHub shows Could not be executed, and its output includes the reason, such as the following:
trust policy places gcs-read at ring 1.trusted, which this pull request does not reach: its opener is in none of subring trusted's authors groupsIf the check is blocking, the summary check fails too, with a title such as 1 check(s) could not be executed.
To keep these failures from blocking pull requests opened by people outside your trust section, make the check advisory, or skip it on those pull requests with a condition. A condition only decides whether the check runs. A pull request can remove a condition that would skip a check, so a condition doesn’t keep an identity from a pull request. The trust section does. A condition on context.github.pull_request.head_repository also skips pull requests that people you trust open from their own forks.
To take access back, change the place that grants it:
| To stop | Change | When it takes effect |
|---|---|---|
| A check from getting an identity | Remove the identity, the check’s selector, or the allow rule from identities.yaml on the base branch, or on your default branch for runs after merging. | New runs, once the change merges. A running check loses the identity at its next renewal, within about five minutes. |
| Pull requests from some people from getting identities | In the trust section on your default branch, remove them from a group, or move identities to a higher ring. | New runs, once the change merges. A running check loses the identity at a later renewal, within about 10 minutes. Chainguard Checks can take up to three minutes to see a change to an organization’s or team’s members on GitHub. |
| Every use of an identity, including runs after merging | On the provider, remove the roles granted to the identity’s subject, or remove the provider’s trust in it, such as by disabling the Google Cloud workload identity pool provider or deleting the Chainguard assumable identity. | The provider refuses new token exchanges. A Google Cloud access token that a check already has can keep working until it expires, for up to an hour. |
Chainguard Workspaces sessions don’t use workload identities. To revoke what a session can reach, refer to Review and revoke a session’s credentials and Your Chainguard identity in a session.
Last updated: 2026-10-13 14:50