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 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 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 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.
Related Jump to heading
- Build Provenance & Attestations β the parent topic and where the chain breaks.
- Generating Provenance for a Tagged Release β producing what this gate checks.
- Linking a Container Image to Its Commit β why deploying by digest is a prerequisite.