Identities#
An identity says who may sign. A keyless identity is an OIDC issuer plus a certificate subject; a keyed identity is a public key. Rules list the identities they accept for the signature and for the SBOM, and any one match passes.
identities:
ci-main: # name used by rules
issuer: https://token.actions.githubusercontent.com # or issuerRegExp
subject: https://github.com/acme/api/.github/workflows/release.yml@refs/heads/main # or subjectRegExp
vendor-key:
publicKey: /etc/imagegate/keys/vendor.pub
imagegate matches the issuer and the subject (the certificate's SAN) only. Other Fulcio certificate extensions, such as the source repository or trigger event, are not checked.
Find who signed an image#
Give a rule a placeholder identity and ask imagegate. The denial names the real signer, after verifying the certificate chain, SCT and Rekor entry:
identities:
probe: { issuer: https://example.invalid, subject: probe }
rules:
- name: probe
images: ["**"]
signature: { identities: [probe] }
sbom: { required: false }
$ imagegate warm cgr.dev/chainguard/static:latest
DENY cgr.dev/chainguard/static@sha256:fe55…: IDENTITY_MISMATCH — signed by
"https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main"
(issuer https://token.actions.githubusercontent.com), which is not an allowed identity
Copy the issuer and subject into a real identity, then tighten the subject to a regex if it contains a version.
Verified examples#
Each of these was checked with imagegate warm against the public image on
2026-10-15. Re-check after a vendor changes its release pipeline.
| Image | Provider | Signed SBOM? | Result |
|---|---|---|---|
cgr.dev/chainguard/static |
GitHub Actions, branch | yes | ALLOWED with SBOM required |
ghcr.io/fluxcd/* |
GitHub Actions, tags + reusable workflow | no, unsigned BuildKit SBOM | ALLOWED signature-only; SBOM_UNTRUSTED if SBOM required |
ghcr.io/aquasecurity/trivy |
GitHub Actions, reusable workflow | no | ALLOWED signature-only |
gcr.io/distroless/* |
Google service account | no | ALLOWED signature-only |
registry.k8s.io/* |
Google service account | no | ALLOWED signature-only |
ghcr.io/sigstore/cosign/cosign |
Google service account | no | ALLOWED signature-only |
identities:
# GitHub Actions on a branch. Signs images and SPDX attestations.
chainguard:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main
# GitHub Actions on version tags. Some Flux controllers release from their
# own workflow, others from a shared reusable workflow in another repo.
flux:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: ^https://github\.com/fluxcd/([a-z-]+/\.github/workflows/release\.yml|gha-workflows/\.github/workflows/controller-release\.yaml)@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$
# A reusable workflow called on tags.
trivy:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: ^https://github\.com/aquasecurity/trivy/\.github/workflows/reusable-release\.yaml@refs/tags/v[0-9.]+$
# Google service accounts (Cloud Build and other Google-run pipelines).
distroless:
issuer: https://accounts.google.com
subject: keyless@distroless.iam.gserviceaccount.com
kubernetes:
issuer: https://accounts.google.com
subject: krel-trust@k8s-releng-prod.iam.gserviceaccount.com
sigstore:
issuer: https://accounts.google.com
subject: keyless@projectsigstore.iam.gserviceaccount.com
rules:
- name: chainguard
images: ["cgr.dev/chainguard/**"]
signature: { identities: [chainguard] }
sbom: { identities: [chainguard] } # signed SPDX attestations exist
- name: flux
images: ["ghcr.io/fluxcd/**"]
signature: { identities: [flux] }
sbom: { required: false } # SBOM attached but unsigned
- name: trivy
images: ["ghcr.io/aquasecurity/trivy"]
signature: { identities: [trivy] }
sbom: { required: false }
- name: distroless
images: ["gcr.io/distroless/**"]
signature: { identities: [distroless] }
sbom: { required: false }
- name: kubernetes
images: ["registry.k8s.io/**"]
signature: { identities: [kubernetes] }
sbom: { required: false }
Reusable workflows sign as themselves
When a GitHub workflow calls a reusable workflow, the certificate
subject is the reusable workflow (job_workflow_ref), not the
caller. That's why Flux's helm-controller is signed by
fluxcd/gha-workflows. Trusting a shared reusable workflow trusts every
repository allowed to call it.
Your own CI#
The subject formats below come from Fulcio's identity configuration. Except for GitHub Actions and Google service accounts, they were not tested end to end here. Confirm yours with the probe above.
GitHub Actions#
Issuer https://token.actions.githubusercontent.com. Subject
https://github.com/<owner>/<repo>/.github/workflows/<file>@<ref>.
identities:
# Only the release workflow on main.
ci-main:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/acme/api/.github/workflows/release.yml@refs/heads/main
# Any repo in the org, release workflow, main branch only.
ci-org-main:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: ^https://github\.com/acme/[^/]+/\.github/workflows/release\.ya?ml@refs/heads/main$
# Semver tags only.
ci-tags:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: ^https://github\.com/acme/api/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$
# A central reusable workflow that every team calls.
platform-release:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: ^https://github\.com/acme/workflows/\.github/workflows/sign\.yml@refs/tags/v[0-9]+$
Pull request builds run on refs/pull/<n>/merge, so anchoring the ref to
refs/heads/main or tags keeps PR builds out.
GitLab CI (gitlab.com)#
Issuer https://gitlab.com. Subject https://gitlab.com/<group>/<project>//<ci config path>@<ref>.
Note the double slash before the CI file.
identities:
gitlab-main:
issuer: https://gitlab.com
subject: https://gitlab.com/acme/api//.gitlab-ci.yml@refs/heads/main
gitlab-group-tags:
issuer: https://gitlab.com
subjectRegExp: ^https://gitlab\.com/acme/[^/]+//\.gitlab-ci\.yml@refs/tags/v[0-9.]+$
Public-good Fulcio accepts only the GitLab instances it lists (gitlab.com and a few others). Self-managed GitLab needs a private Sigstore.
Buildkite#
Issuer https://agent.buildkite.com. Subject https://buildkite.com/<organization>/<pipeline>.
identities:
buildkite-release:
issuer: https://agent.buildkite.com
subject: https://buildkite.com/acme/api-release
CircleCI#
Issuer https://oidc.circleci.com/org/<organization-id>. Subject
https://circleci.com/api/v2/projects/<project-id>/pipeline-definitions/<pipeline-definition-id>.
identities:
circleci-release:
issuer: https://oidc.circleci.com/org/00000000-0000-0000-0000-000000000000
subjectRegExp: ^https://circleci\.com/api/v2/projects/11111111-1111-1111-1111-111111111111/pipeline-definitions/[0-9a-f-]+$
Codefresh#
Issuer https://oidc.codefresh.io. Subject
<platform url>/<account name>/<pipeline name>:<account id>/<pipeline id>.
Use the probe to read the exact value for your account.
Google Cloud (service accounts)#
Issuer https://accounts.google.com, subject the service account's email.
This covers Cloud Build and anything else that signs as a service account.
identities:
cloudbuild:
issuer: https://accounts.google.com
subject: release-signer@acme-prod.iam.gserviceaccount.com
Kubernetes workloads#
Pods signing with their service account token (EKS, GKE, AKS clusters that
Fulcio trusts) use the cluster's OIDC issuer URL and the subject
https://kubernetes.io/namespaces/<namespace>/serviceaccounts/<name>.
identities:
tekton-signer:
issuer: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE0123456789
subject: https://kubernetes.io/namespaces/tekton-chains/serviceaccounts/tekton-chains-controller
People#
cosign sign run interactively signs with an email address. The issuer is
the upstream provider chosen at login:
| Login | issuer |
|---|---|
| GitHub | https://github.com/login/oauth |
https://accounts.google.com |
|
| Microsoft | https://login.microsoftonline.com |
Prefer CI identities for anything automated: a person's identity can sign from any machine.
Keys#
Keyed identities take a PEM public key: ECDSA, RSA or Ed25519.
cosign generate-key-pair # cosign.key + cosign.pub
cosign public-key --key awskms:///alias/image-signing > aws.pub
cosign public-key --key gcpkms://projects/acme/locations/global/keyRings/ci/cryptoKeys/sign > gcp.pub
cosign public-key --key azurekms://acme-vault.vault.azure.net/sign > azure.pub
cosign public-key --key hashivault://image-signing > vault.pub
Keyed signatures made with --tlog-upload=false (or a cosign v3 signing
config without Rekor) verify by key alone. If they carry a Rekor entry,
it's checked. The policy hash includes each key file's contents, so
replacing a key at the same path invalidates cached verdicts.
Patterns#
Rotate without downtime. List the old and new identity together, deploy, switch CI to the new one, then remove the old.
Separate signer and SBOM attester. The SBOM can come from a different pipeline, such as a security team's scanner:
Write regexes defensively.
- Anchor with
^…$; regexes match anywhere in the subject, so without$,@refs/heads/mainalso matches@refs/heads/main-hotfix. - Escape dots (
github\.com,\.github). - Use
[^/]+for a single path segment, not.+. - Pin the ref: a branch, or a tag pattern.
Private Sigstore#
For a private Fulcio and Rekor, point trust.sigstore at their
trusted_root.json. If your Fulcio has no CT log, also set
requireSCT: false.
Limitations#
- Signatures in another repository. Some projects store signatures
outside the image's repository (
COSIGN_REPOSITORY); Kyverno usesghcr.io/kyverno/signatures. imagegate looks only in the image's own repository, so these images showNO_SIGNATURE. Mirror and re-sign them. - Notation (Notary v2) signatures are not supported, only cosign and Sigstore bundles.