Verifying a release
Every release is built by .github/workflows/release.yml on a v* tag, in
GitHub Actions, and signed there with cosign keyless: the signing
certificate is issued by Sigstore's Fulcio to that workflow's GitHub OIDC
identity and logged in the Rekor transparency log. No long-lived signing key
exists. What you can check:
| Artifact | Signed | SBOM | Provenance |
|---|---|---|---|
ghcr.io/guard-spec/guard-spec-gateway |
cosign, the index and each platform image | SPDX 2.3 per platform (BuildKit attestation) | SLSA v1 per platform (BuildKit, mode=max) |
ghcr.io/guard-spec/guard-spec-gateway-full |
cosign, the index and each platform image | SPDX 2.3 per platform | SLSA v1 per platform (mode=max) |
ghcr.io/guard-spec/guard-spec-control-plane |
cosign (GoReleaser) | SPDX 2.3 per platform | SLSA v1 per platform, minimal (no builder id) |
Release archives (guard-spec_<version>_<os>_<arch>.tar.gz) |
checksums.txt + checksums.txt.sigstore.json |
<archive>.sbom.json (SPDX, syft) |
— |
Linux _onnx archives |
checksums-onnx.txt + checksums-onnx.txt.sigstore.json |
<archive>.sbom.json |
— |
| Models | SHA-256 pinned in models/manifest.yaml (and build/gateway-full.l2.sha256 for the image) |
— | — |
You need cosign
2.4 or newer (the outputs below are from cosign v3.1.4), docker buildx
for the attestations, jq, and sha256sum (shasum -a 256 on macOS).
1. Image signatures
Pin the identity to the release workflow at the tag you deploy. That proves the image was signed by this repository's release workflow running for that tag, not by a branch, a fork or another workflow:
VERSION=0.1.1
for img in guard-spec-gateway guard-spec-gateway-full guard-spec-control-plane; do
cosign verify "ghcr.io/guard-spec/$img:$VERSION" \
--certificate-identity "https://github.com/guard-spec/guard-spec/.github/workflows/release.yml@refs/tags/v$VERSION" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
| jq -r '.[0].critical.image."docker-manifest-digest"'
done
Output for v0.1.1 (run on 2026-11-06):
Verification for ghcr.io/guard-spec/guard-spec-gateway:0.1.1 --
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 certificates
sha256:948d14a0441e40a97ef83dd4824c4ffb146817e72fa125a3dab711af27b62729
Verification for ghcr.io/guard-spec/guard-spec-gateway-full:0.1.1 --
(same three checks)
sha256:d0617aef2be4f15d6778541240bae774dbeaab8c79ce8c9dc4904414b1be994e
Verification for ghcr.io/guard-spec/guard-spec-control-plane:0.1.1 --
(same three checks)
sha256:c689d71a3dcd695bec13d8527a35b1ca97aa6563d2f0047e5615c15c3119ea93
A wrong identity fails, which is what makes the check meaningful:
$ cosign verify ghcr.io/guard-spec/guard-spec-gateway:0.1.1 \
--certificate-identity https://github.com/guard-spec/guard-spec/.github/workflows/release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Error: no matching attestations: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "https://github.com/guard-spec/guard-spec/.github/workflows/release.yml@refs/heads/main", got "https://github.com/guard-spec/guard-spec/.github/workflows/release.yml@refs/tags/v0.1.1"
SECURITY.md shows a looser form,
--certificate-identity-regexp '^https://github.com/guard-spec/guard-spec/',
which accepts any workflow of the repository. Prefer the exact identity.
Deploy by digest. A tag can be moved; a digest cannot. Use the digest
printed above in your manifests
(image: ghcr.io/guard-spec/guard-spec-gateway-full@sha256:...). With the
Helm chart, set the tag to 0.1.1@sha256:... (the chart renders
<repository>:<tag>); the Terraform reference
has *_image_digest variables. In Kubernetes, an admission controller such as the Sigstore
policy-controller or Kyverno can enforce the same identity on every pull.
2. SBOMs
The image SBOMs are BuildKit attestations inside the image index, one per platform:
docker buildx imagetools inspect ghcr.io/guard-spec/guard-spec-gateway:0.1.1 \
--format '{{json .SBOM}}' > gateway-sbom.json
jq -r 'to_entries[] | "\(.key) \(.value.SPDX.spdxVersion) packages=\(.value.SPDX.packages|length)"' gateway-sbom.json
linux/amd64 SPDX-2.3 packages=169
linux/arm64 SPDX-2.3 packages=169
The same for v0.1.1's other images: guard-spec-gateway-full 169 packages
per platform, guard-spec-control-plane 65. Feed .["linux/amd64"].SPDX
to your scanner (for example grype sbom:./sbom.spdx.json). The archive
SBOMs are files on the release, covered by the signed checksums below.
3. SLSA provenance
docker buildx imagetools inspect ghcr.io/guard-spec/guard-spec-gateway:0.1.1 \
--format '{{json .Provenance}}' > gateway-provenance.json
jq -r 'to_entries[] | "\(.key) builder=\(.value.SLSA.runDetails.builder.id) revision=\(.value.SLSA.buildDefinition.externalParameters.request.args["label:org.opencontainers.image.revision"])"' gateway-provenance.json
git rev-list -n 1 v0.1.1 # in a clone of guard-spec/guard-spec
linux/amd64 builder=https://github.com/guard-spec/guard-spec/actions/runs/42172842975/attempts/1 revision=d653cb29edb009a0a14c4871616aa21c7d208069
linux/arm64 builder=https://github.com/guard-spec/guard-spec/actions/runs/42172842975/attempts/1 revision=d653cb29edb009a0a14c4871616aa21c7d208069
d653cb29edb009a0a14c4871616aa21c7d208069
The provenance names the workflow run that built the image and the commit
it built, which must be the commit of the tag. guard-spec-gateway-full
gives the same run and revision. With mode=max it also lists the
Dockerfile, build arguments and every base image by digest
(.buildDefinition.resolvedDependencies).
The control-plane image is built by GoReleaser with a minimal provenance (no builder id). Its signature (step 1) already binds it to the release workflow at the tag, and its labels name the commit:
docker buildx imagetools inspect ghcr.io/guard-spec/guard-spec-control-plane:0.1.1 \
--format '{{json .Image}}' | jq -r '."linux/amd64".config.Labels."org.opencontainers.image.revision"'
# d653cb29edb009a0a14c4871616aa21c7d208069
These attestations are unsigned BuildKit documents stored next to the image; trust them because the image index that references them is the one whose signature you verified in step 1.
4. Release archives
VERSION=0.1.1
gh release download "v$VERSION" -R guard-spec/guard-spec \
-p 'checksums*' -p "guard-spec_${VERSION}_linux_amd64*"
for f in checksums.txt checksums-onnx.txt; do
cosign verify-blob "$f" --bundle "$f.sigstore.json" \
--certificate-identity "https://github.com/guard-spec/guard-spec/.github/workflows/release.yml@refs/tags/v$VERSION" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
done
sha256sum --ignore-missing -c checksums.txt checksums-onnx.txt
Verified OK
Verified OK
guard-spec_0.1.1_linux_amd64.tar.gz: OK
guard-spec_0.1.1_linux_amd64.tar.gz.sbom.json: OK
guard-spec_0.1.1_linux_amd64_onnx.tar.gz: OK
guard-spec_0.1.1_linux_amd64_onnx.tar.gz.sbom.json: OK
checksums.txt covers the GoReleaser archives, their SBOMs and the source
archive; checksums-onnx.txt covers the linux _onnx archives and their
SBOMs.
5. Models
models pull checks every file against the SHA-256 in
models/manifest.yaml of the release you run and refuses a mismatch; see
docs/models.md. The gateway-full image checks the L2 files
against build/gateway-full.l2.sha256 while it is built, and its signature
covers the result.
What this does not prove
- That the source is free of vulnerabilities. Scan the SBOMs; CI runs
govulncheck, CodeQL and OpenSSF Scorecard on the repository. - That a build is reproducible bit for bit. Binaries are built with
-trimpathand the commit timestamp, but nobody has published an independent rebuild yet. - Anything about images you rebuilt or mirrored: verify before mirroring, and deploy the digest you verified.
Source: docs/security/verifying-releases.md @ ca69dad29f02