Verification summary & policy
VSAs and requiring multiple claims before trust.
In early 2024 a trusted maintainer's credentials were used to hide a backdoor inside XZ Utils. The poisoned release was built, signed and published exactly like every clean one before it, and the whole chain of automated checks waved it through. What saved the ecosystem was one engineer downstream who bothered to look at the evidence instead of trusting the signature. Now scale that job up. You run a platform with 60 Kubernetes clusters, several of them air-gapped (no network route out to the internet at all), and every one of them is supposed to check an image's provenance, its SBOM (software bill of materials, the list of what went into the build) and its scan results before letting the workload run. Each of those checks needs the raw attestations, the Fulcio root certificates, and a live lookup against the Rekor transparency log, which is Sigstore's public append-only ledger of everything that has been signed. That is expensive at the edge and flatly impossible where there is no egress. A Verification Summary Attestation (VSA) works like the inspection certificate stapled inside a lift: one authority you trust does the full inspection once, and everyone after that reads the signed verdict instead of pulling the panels off again.
Verify once, trust cheaply after
A VSA is an in-toto attestation, meaning a signed statement about one specific artifact written in the shared format all the supply-chain tools speak, and its predicate type is https://slsa.dev/verification_summary/v1. Three documents, three different questions. SLSA provenance (Supply-chain Levels for Software Artifacts, the framework that grades how tamper-resistant a build was) answers 'how was this built'. An SBOM answers 'what is inside'. A VSA answers something else entirely: 'did a verifier I trust already check this artifact against a named policy, and what did it decide?' Two fields carry all the weight. verificationResult is either PASSED or FAILED. verifiedLevels lists the SLSA levels the artifact actually reached, for example SLSA_BUILD_LEVEL_3. The consumer stops assembling provenance and SBOM and scan results for itself. It checks one signature and reads one verdict. You pay for that convenience with concentrated trust. A VSA is worth exactly as much as the authority that signed it and the policy that authority ran, so that authority stops being a convenience script somebody wrote on a Friday afternoon and becomes reviewed, load-bearing infrastructure. Every cluster downstream is now standing on it.
What each field in the predicate is doing
Every field in there earns its place. The Statement's subject nails the verdict to one exact artifact digest, so nobody can peel a VSA off one image and stick it on another. Inside the predicate, verifier.id names who is making the claim. policy.uri and policy.digest pin the precise policy version that ran, so when you rotate the policy the old VSAs stop speaking for the new rules, which is the behaviour you want. inputAttestations records the digests of the provenance, SBOM and scan attestations that were genuinely evaluated, which keeps the decision auditable: anyone can fetch those same inputs, run the evaluation again and see whether they land on the same answer. verifiedLevels lists the tracks the artifact reached. dependencyLevels can summarise how many transitive dependencies met each level. slsaVersion fixes which schema is in play, and timeVerified stamps when the check ran. What the VSA leaves out on purpose is the evidence itself, and that missing bulk is the entire reason it fits down a thin pipe into a locked-down cluster.
{"_type": "https://in-toto.io/Statement/v1","subject": [{ "name": "ghcr.io/acme/api","digest": { "sha256": "6f8e2c1b9a...d41" } }],"predicateType": "https://slsa.dev/verification_summary/v1","predicate": {"verifier": { "id": "https://acme.dev/platform/verifier" },"timeVerified": "2026-07-16T09:14:52Z","resourceUri": "oci://ghcr.io/acme/api@sha256:6f8e2c1b9a...d41","policy": {"uri": "https://acme.dev/policy/prod-admission@v7","digest": { "sha256": "b2d4a1...9f0" }},"inputAttestations": [{ "uri": "provenance.intoto.jsonl", "digest": { "sha256": "1a2b3c...d3e" } },{ "uri": "sbom.cdx.intoto.jsonl", "digest": { "sha256": "4e5f6a...a6b" } },{ "uri": "scan.vex.intoto.jsonl", "digest": { "sha256": "7c8d9e...e9f" } }],"verificationResult": "PASSED","verifiedLevels": [ "SLSA_BUILD_LEVEL_3" ],"slsaVersion": "1.0"}}
That Statement never gets signed as it stands. Every in-toto attestation travels inside a DSSE envelope (Dead Simple Signing Envelope). The JSON Statement is base64-encoded into a payload field, labelled with a payloadType, and then the signature is computed over something slightly different from the payload: its Pre-Authentication Encoding, or PAE. The PAE is the byte string DSSEv1 SP len(payloadType) SP payloadType SP len(payload) SP payload, where SP is a single space and each len is the byte length written out in ASCII decimal. It works like writing the document type and the exact page count on the outside of a sealed envelope. Because the type and both lengths sit inside the signed bytes, an attacker cannot lift a signature made over one predicate type and re-present those identical bytes claiming a different type, and cannot splice fields from two payloads together. With keyless signing there is no static key id at all. Riding alongside the signature instead is a short-lived certificate from Fulcio, Sigstore's certificate authority, which issues certificates that live about ten minutes and record the identity that asked for one. That certificate is exactly what the downstream gate will pin against.
{"payloadType": "application/vnd.in-toto+json","payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLC4uLn0=","signatures": [{"keyid": "","sig": "MEUCIQDx8k...base64-ecdsa-P256...Qm9Zr2==","cert": "-----BEGIN CERTIFICATE-----\nMIIC...Fulcio-issued, ~10 min validity, SAN = signing workflow identity...\n-----END CERTIFICATE-----"}]}# The signed bytes are the PAE, NOT the raw payload:# DSSEv1 SP 28 SP application/vnd.in-toto+json SP 812 SP <payload-bytes># payloadType is folded into the signed PAE -> a sig cannot be replayed under another type.
Minting the VSA at the authority
The authority pays the full cost, once. Its verifier job starts by genuinely checking the provenance for the target digest with slsa-verifier, confirming the builder identity, the source repository, and that the provenance's own signature really is recorded in Rekor. In a real pipeline it checks the SBOM and the scan attestations the same way. Only when all of that passes does it build the VSA and sign it keyless with cosign attest, which uploads the signed DSSE envelope to Rekor and attaches it to the image. If verification fails, no VSA comes out, or one comes out carrying verificationResult FAILED. Either way the verdict is honest, because the signer only ever signs what it actually checked. A verifier that signs first and checks later is worse than having no verifier at all, since it manufactures confidence that nobody earned and hands it to 60 clusters.
# 1) Actually verify the SLSA provenance for this exact digest$ slsa-verifier verify-image ghcr.io/acme/api@sha256:6f8e2c1b9a...d41 \--source-uri github.com/acme/api \--builder-id 'https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml'Verified signature against tlog entry index 148203402 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb2...c3f0Verified build using builder https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0 at commit a3f0c7e1b2...PASSED: Verified SLSA provenance# 2) Only on PASS: sign the VSA keyless and log it to Rekor.# cosign builds the in-toto Statement wrapper itself (subject = the image,# predicateType = --type), so feed it ONLY the predicate body:$ jq '.predicate' vsa.json > vsa.predicate.json$ cosign attest --yes --predicate vsa.predicate.json \--type https://slsa.dev/verification_summary/v1 \ghcr.io/acme/api@sha256:6f8e2c1b9a...d41Generating ephemeral keys...Retrieving signed certificate...Successfully verified SCT...tlog entry created with index: 148203471
At the gate: one signature, one decision
Downstream, the gate re-derives nothing. It runs cosign verify-attestation pinned to the verifier's certificate identity and OIDC issuer (OpenID Connect, the identity system that vouched for whoever asked Fulcio for a certificate). That pinning is the load-bearing step, because a VSA's whole authority comes from who signed it. The pinned identity lives in the certificate's SAN, the Subject Alternative Name field. For a verifier running as a GitHub Actions reusable workflow the SAN is the workflow URI, .../mint-vsa.yml@refs/heads/main, paired with the token.actions.githubusercontent.com issuer. That pairing is a cryptographic fact the signer cannot invent, and it is deliberately separate from the free-form verifier.id string sitting inside the predicate, which is only a human-facing label. Anybody can type any string into verifier.id. Nobody can fake the certificate. cosign hands back only attestations whose subject matches the image digest being admitted, so digest binding comes for free and a VSA cannot be replayed onto another artifact. The decoded predicate then goes to a policy engine, which makes the real admit or deny call on two conditions: verificationResult is PASSED, and verifiedLevels contains the required SLSA_BUILD_LEVEL_3. Everything else denies. A missing VSA denies, a VSA signed by the wrong identity denies, a FAILED result denies, a level under the bar denies. One signature check plus one policy evaluation replaces fetching and validating three separate attestations against a live transparency log, which is precisely what makes this workable inside an air-gapped cluster.
$ cosign verify-attestation \--type https://slsa.dev/verification_summary/v1 \--certificate-identity 'https://github.com/acme/platform/.github/workflows/mint-vsa.yml@refs/heads/main' \--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ghcr.io/acme/api@sha256:6f8e2c1b9a...d41 \| jq -r '.payload | @base64d | fromjson | .predicate| "\(.verificationResult) \(.verifiedLevels[0])"'Verification for ghcr.io/acme/api@sha256:6f8e2c1b9a...d41 --The following checks were performed on each of these signatures:- The cosign claims were validated- Existence of the claims in the transparency log was verified offline- The code-signing certificate was verified using trusted certificate authority certificatesPASSED SLSA_BUILD_LEVEL_3
# vsa-gate.regopackage admissiondefault allow := falseallow if {input.verificationResult == "PASSED""SLSA_BUILD_LEVEL_3" in input.verifiedLevels}# --- run it: feed the verified predicate straight into the gate ---$ cosign verify-attestation --type https://slsa.dev/verification_summary/v1 \--certificate-identity 'https://github.com/acme/platform/.github/workflows/mint-vsa.yml@refs/heads/main' \--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ghcr.io/acme/api@sha256:6f8e2c1b9a...d41 2>/dev/null \| jq '.payload | @base64d | fromjson | .predicate' \| opa eval -I -f pretty -d vsa-gate.rego 'data.admission.allow'true# A FAILED result, a level below BUILD_LEVEL_3, or a signature from any other# identity -> cosign errors or the query returns false -> the gate denies.
Every gram of trust in this design rests on one question: was this VSA signed by the verifier you trust, and by nobody else? Pin the wrong identity, or forget to pin at all, and the scheme flips inside out, because now anyone at all can mint a PASSED verdict for anything they like. That is why the next lesson, cosign: sign and verify by identity, opens up exactly how keyless signing ties a signature to a workflow identity, what really sits inside the Fulcio certificate, and how to pin certificate-identity and issuer so a self-signed attestation is thrown out on sight.
Try this
Work through “At the gate: one signature, one decision” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: pASSED means passed then, not passed now. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.