Verifying attestations before deploy Jump to heading

The difference between a verification gate that works and one that is decorative is three flags. cosign verify-attestation with no identity constraints accepts an attestation signed by anyone, for any repository, which means an attacker who can run a workflow in any repository anywhere can produce something your gate approves. Pinning the identity, the issuer and the source repository turns the same command into a real control. This recipe writes the strict version and puts it somewhere it cannot be bypassed, within build provenance and attestations.

When to use this approach Jump to heading

  • Attestations are being generated and nothing currently checks them.
  • A deployment path exists that does not go through your main pipeline.
  • You need to demonstrate that unverified artefacts cannot reach production.
  • An audit has asked what prevents an arbitrary image being deployed.
  • If nothing produces attestations yet, start with generating provenance for a tagged release.

Step 1 β€” Write the strict verification Jump to heading

Each constraint closes a different hole.

cosign verify-attestation \
  --type slsaprovenance \
  --certificate-identity-regexp '^https://github\.com/acme/app/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  "ghcr.io/acme/app@${DIGEST}"
# The forge-native equivalent, which pins owner and repository
gh attestation verify "oci://ghcr.io/acme/app@${DIGEST}" \
  --owner acme --repo acme/app \
  --signer-workflow acme/app/.github/workflows/release.yml
# Verification: an artefact from another repository must fail
cosign verify-attestation --type slsaprovenance \
  --certificate-identity-regexp '^https://github\.com/acme/app/' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/someone-else/app:latest 2>&1 | tail -2
What each constraint closesWithout an identity pattern, any workflow in any repository satisfies the gate. Without an issuer, a signature from a different identity provider is accepted. Without a repository pin, a workflow in an unrelated repository of your own organisation is enough.Identity regexpwhich workflow signedand from which refOIDC issuerwhich providernot any providerRepositorywhich sourcenot any repoomitting any one of the three makes the gate satisfiable by something you did not intend

Note that the identity pattern includes the ref: @refs/tags/v means the attestation must have been produced by that workflow running on a version tag, which closes the case of a branch build producing a technically valid attestation.

Step 2 β€” Fail closed, in the pipeline Jump to heading

A verification step whose failure is swallowed is worse than none, because it produces a false record of checking.

#!/usr/bin/env sh
set -euo pipefail          # a non-zero exit must stop everything after it

digest=$(crane digest "ghcr.io/acme/app:${VERSION}")
cosign verify-attestation --type slsaprovenance \
  --certificate-identity-regexp '^https://github\.com/acme/app/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  "ghcr.io/acme/app@${digest}" > /dev/null

kubectl set image deploy/app "app=ghcr.io/acme/app@${digest}" -n production
# Verification: deliberately break the digest and confirm nothing deploys
DIGEST=sha256:0000000000000000000000000000000000000000000000000000000000000000 \
  sh deploy.sh 2>&1 | tail -2; echo "exit: $?"

SAFETY WARNING β€” verification must happen against the digest that will be deployed, resolved once. Resolving a tag, verifying it, and then deploying the tag leaves a window in which the tag can be repointed between the two operations, and the deployment then runs an artefact that was never verified. Resolve to a digest first and use that digest for both the check and the deployment.

Step 3 β€” Move the gate into the cluster Jump to heading

A pipeline gate protects the path you control. An admission gate protects every path.

# A policy shape β€” the specific controller varies (Kyverno, Gatekeeper, Connaisseur)
# Deny any image in the production namespace whose provenance does not verify
# against the expected identity and repository.
#
#   match:   namespace = production
#   verify:  attestation type slsaprovenance
#            identity   ^https://github.com/acme/app/.github/workflows/release.yml@refs/tags/v
#            issuer     https://token.actions.githubusercontent.com
#   action:  deny
# Verification: an arbitrary public image is refused by the cluster, not the pipeline
kubectl run probe --image=nginx:latest -n production --dry-run=server 2>&1 | tail -2
# And a properly attested image is admitted
kubectl run probe --image="ghcr.io/acme/app@${DIGEST}" -n production --dry-run=server 2>&1 | tail -1
A pipeline gate against an admission gateA pipeline gate is simple and covers the deployments that go through it. An admission gate covers every path into the cluster, including a manual kubectl at three in the morning, which is precisely the path that matters during an incident.Pipeline gateAdmission gatecovers the pipelineyesyescovers manual deploysnoyescovers other pipelinesnoyessetup efforta scripta controllerthe second row is the one that decides whether the control is real

Step 4 β€” Decide what an unverifiable artefact means Jump to heading

The gate needs a documented answer for the case where verification cannot run at all.

# Distinguish "verification failed" from "verification could not run"
if ! out=$(cosign verify-attestation --type slsaprovenance \
            --certificate-identity-regexp "$IDENTITY" \
            --certificate-oidc-issuer "$ISSUER" "$IMAGE" 2>&1); then
  case "$out" in
    *"no matching attestations"*) echo "REFUSE: no provenance for $IMAGE" >&2; exit 1 ;;
    *"connection refused"*|*"timeout"*) echo "REFUSE: verification unavailable" >&2; exit 1 ;;
    *) echo "REFUSE: $out" >&2; exit 1 ;;
  esac
fi
# Verification: an unreachable transparency log stops the deployment
COSIGN_REKOR_URL=https://127.0.0.1:1 sh deploy.sh 2>&1 | tail -2

Both branches refuse, deliberately. A gate that deploys when it cannot verify is a gate that an attacker can defeat by making verification fail, which is usually easier than defeating the verification itself.

Step 5 β€” Provide a break-glass path that is visible Jump to heading

A gate with no exception will be removed the first time it blocks an incident response.

# An explicit, logged override β€” never an environment variable nobody notices
if [ "${DEPLOY_BREAK_GLASS:-}" = "INC-4471" ]; then
  echo "::warning::provenance verification bypassed for INC-4471 by ${USER}" >&2
  printf '%s\t%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$USER" "INC-4471" "$IMAGE" >> ~/incidents/bypasses.tsv
else
  verify_or_exit
fi
# Verification: bypasses are findable afterwards
cat ~/incidents/bypasses.tsv 2>/dev/null | tail -5
The gate, and the two ways past itA verified artefact deploys. An unverified one is refused, whether verification failed or could not run. The only other route is a break-glass override that requires an incident identifier and writes an audit line, so an exception is visible rather than silent.Resolve digestonce, not per stepVerifyidentity, issuer, repoDeploy or refusefail closedBreak glassincident idaudit linethe fourth box exists so the third one is never removed under pressure

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Is verifying the signature enough without the identity? Jump to heading

No, and this is the most common way a gate is decorative. A valid signature proves someone signed it; the identity constraint is what proves it was your release workflow rather than any workflow anywhere. Keyless signing makes the identity checkable, which is most of its value β€” a gate that does not check it discards that value.

What about images we do not build β€” base images and sidecars? Jump to heading

They need a separate policy, because you cannot require your own identity on someone else’s artefact. The usual approach is an allowlist of trusted external images pinned by digest, verified against the publisher’s own signing identity where they provide one. Treat the allowlist as a reviewed document, not as a convenience.

Does the transparency log have to be reachable at deploy time? Jump to heading

For full verification, yes, which is why Step 4 treats unavailability as a refusal. Some setups verify against a bundled signature and check the log asynchronously, which trades a weaker deploy-time check for availability. That is a legitimate trade if it is made deliberately and the asynchronous check actually alerts.