SBOM & Compliance

Cosign Verification Policies in Production

Writing cosign verification policies that actually pass production deployment gates requires more precision than the examples suggest. Here is what we have learned.

Shadab Khan
Security Engineer
6 min read

The cosign documentation has good examples for signing. It has fewer good examples for verifying at production scale. The examples are typically shaped like "here is how to verify one image signed by one identity," which is fine for a demo and insufficient for a fleet where hundreds of repositories are signed by dozens of workflows running across several GitHub organisations. The gap between those two situations is where most verification policies become either too loose (pass everything signed by anyone) or too brittle (break every time a workflow filename changes).

This post is a set of patterns we use for cosign verification policies in production. It assumes cosign v2.2 or later, Sigstore keyless signing, and verification performed either at CI pull time or at Kubernetes admission. The examples use cosign verify and cosign verify-attestation, but the same principles apply to the Sigstore policy controller.

What does a minimum viable verification look like?

The minimum verification for a keyless-signed image requires three claims to be checked. The image must be signed. The signing certificate must have been issued by Fulcio. And the certificate's OIDC identity claims must match the expected workflow. The cosign flags for this are --certificate-identity, --certificate-oidc-issuer, and optionally --certificate-identity-regexp when you need to match a pattern.

A common mistake is to use only --certificate-oidc-issuer=https://token.actions.githubusercontent.com and skip the identity check. This verifies that the signature came from some GitHub Actions workflow, anywhere on GitHub, which is essentially no restriction at all. Any public repository's workflow can sign with that issuer. The identity must be pinned.

The right pattern for GitHub Actions is --certificate-identity=https://github.com/ORG/REPO/.github/workflows/WORKFLOW.yml@refs/tags/vSEMVER when verifying a release. For continuous verification where the tag varies, use --certificate-identity-regexp=^https://github.com/ORG/REPO/.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$. The regex is important: a naive .* on the ref allows any branch or tag, including attacker-pushed tags to forks if the workflow is reusable from forks.

How do you handle reusable workflows?

Reusable workflows break the simple identity pattern. When a caller workflow invokes a reusable workflow from another repository, the signing identity is the reusable workflow's path, not the caller's. A release signed via slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml will have that path in its certificate identity regardless of which repository actually triggered the build.

The fix is two-layered matching. Pin the --certificate-identity to the reusable workflow and then separately check the caller via the certificate's OIDC claims. The source_repository_uri and source_repository_ref claims in the Fulcio certificate record the caller's repository and ref. cosign v2.2 exposes these through --certificate-github-workflow-repository and --certificate-github-workflow-ref.

For the slsa-github-generator, a complete verification of a release signed via the reusable workflow looks like: --certificate-identity-regexp='^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3\.yml@refs/tags/v[0-9]+\.' --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-github-workflow-repository=myorg/myrepo --certificate-github-workflow-ref=refs/tags/v1.2.3.

This pins both the builder identity and the caller context, which is what you actually want.

What about attestation verification?

cosign verify-attestation adds a layer on top of image signature verification. It verifies that the attestation is signed, that the signing identity matches policy, and that the attestation's predicate matches a specified type. The flag --type=slsaprovenance1 maps to https://slsa.dev/provenance/v1; --type=spdxjson maps to https://spdx.dev/Document/v2.3.

The gotcha is that cosign verify-attestation passes as soon as one attestation matches. If the image has five attestations of different types and your policy asks for SPDX, cosign returns success when the SPDX one verifies, regardless of whether the other four are valid. For defensive verification you need to verify each attestation type independently and fail if any verification fails.

For SLSA provenance specifically, slsa-verifier verify-image is a better tool than cosign verify-attestation --type=slsaprovenance1 because slsa-verifier understands the provenance structure. It can check that the buildDefinition.externalParameters.source matches an expected repository, that buildDefinition.buildType is the expected builder type URI, and that the subject digest matches the image. cosign cannot do the structural check; it only verifies the envelope signature and the predicate type string.

How do you run this at Kubernetes admission?

The Sigstore policy controller at sigstore/policy-controller (v0.10+ as of July 2024) is the standard way to verify images at Kubernetes admission. It uses ClusterImagePolicy CRDs to declare what must be true about any image before it can run. The CRD supports keyless identity matching, predicate type filtering, and now (v0.9+) CIP-level attestation policies.

The policy-controller failure mode we see most often is "admission works in dev but fails in prod" because the dev cluster has public internet access to rekor.sigstore.dev and the prod cluster does not. policy-controller needs to fetch the Rekor transparency log entry, the Fulcio cert chain, and the TUF metadata. In an air-gapped prod cluster, configure the TrustRoot CRD (added in v0.8) to point at internally mirrored Sigstore trust material.

The other common failure is that the policy matches more images than intended. Policies apply to namespaces (or cluster-wide) and match images by glob or regex. A glob like ghcr.io/myorg/* will match base images pulled by side-car containers that your team does not control. Scope the match selectors tightly, and include an explicit mode: warn initially to surface which images would fail before you switch to mode: enforce.

What about revocation?

Cosign verification has no built-in revocation check, and this surprises people. If a signing identity is compromised and you need to invalidate signatures it produced, there is no "certificate revocation list" equivalent. The remediation is to update policy to no longer trust that identity, and to rebuild and re-sign artifacts that need to remain trusted.

For short-lived Fulcio certificates (10 minutes by default) this is less of an operational problem than it sounds. The certificate itself is long expired by the time you would revoke it. What matters is the identity binding: if the OIDC subject is compromised, you change the policy to reject signatures with that subject, even if the Rekor inclusion proof is valid.

For longer-lived scenarios (cosign with a managed key pair, for example), there is no clean revocation story. Teams that cannot tolerate this either stay on keyless signing or implement their own policy layer that checks a denylist before accepting cosign-verified signatures.

How Safeguard Helps

Safeguard wraps cosign and slsa-verifier with a policy layer that validates identities, attestation types, and caller context in a single rule rather than requiring separate verify commands for each claim. Our verifier understands the slsa-github-generator reusable workflow patterns and automatically pins the builder identity while checking the caller's repository and ref. When a compromised identity needs to be denied, you update one policy and every future verification across CI, registry, and admission controller picks up the change. For Kubernetes admission we provide a policy-controller compatibility layer so customers running ClusterImagePolicy get the same evaluation semantics as customers using Safeguard's own gates.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.