Verify a release — operator runbook¶
Audience: operator or auditor who just pulled a TLSStress.Art release and wants to verify it came from us, hasn't been tampered with, and matches the SBOM we published.
Outcome: in ~5 minutes you confirm (a) every container image carries a valid Cosign signature linked to this GitHub repo, (b) the SHA-256 of the SBOM matches what the release page advertises, and (c) the SBOM's package set matches what's actually inside the running image.
Prerequisites¶
cosign≥ 2.0 — install: https://docs.sigstore.dev/cosign/installationsyft≥ 1.0 — install:curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/binjq— most distros:apt install jq/brew install jq- The release tag you're verifying (e.g.
v3.7.0)
Step 1 — Pull the image you intend to verify¶
Pick any image you actually run. Example: the dashboard.
TAG=v3.7.0
IMG=ghcr.io/nollagluiz/web-agent-dashboard:${TAG}
docker pull "${IMG}"
Step 2 — Verify the Cosign signature is from this repo¶
Cosign keyless OIDC sigs encode the workflow + repo + ref that signed the image. We check the certificate identity matches our release workflow exactly.
cosign verify "${IMG}" \
--certificate-identity-regexp "^https://github.com/nollagluiz/AI_forSE/\\.github/workflows/release\\.yml@refs/tags/${TAG}\$" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
Expected: JSON output ending in Verification successful. If
you get an error, do not run the image — it may be a typosquat
or a tampered re-push.
Common identity-regexp gotchas:
- The
refs/tags/${TAG}part — must match the release tag you pulled. If you pulled:latestsubstitute the resolved tag fromdocker inspect. - The
--certificate-oidc-issueris always the GitHub Actions issuer for keyless OIDC sigs — never change this.
Step 3 — Verify the GHCR attestation¶
GitHub Actions native attestation (via actions/attest-build-provenance)
gives us a second independent check.
gh attestation verify "oci://${IMG}" \
--repo nollagluiz/AI_forSE \
--predicate-type "https://spdx.dev/Document"
Expected: Loaded digest sha256:... for oci://... then
Verification successful!. If this fails but Cosign succeeded,
the most likely cause is the slim-SBOM attestation hitting the
16 MiB API cap — that's tolerated in the release pipeline.
Step 4 — Pull the full SPDX SBOM from the release page¶
gh release download "${TAG}" \
--repo nollagluiz/AI_forSE \
--pattern "dashboard-sbom.spdx.json"
Or via web: https://github.com/nollagluiz/AI_forSE/releases/tag/v3.7.0
→ Assets section → click the <image>-sbom.spdx.json file.
Step 5 — Verify the SBOM matches the running image¶
Generate an SBOM yourself from the image you pulled, then diff the package set against ours.
syft "${IMG}" -o spdx-json > /tmp/local-sbom.spdx.json
# Compare the package list (name + version). Output should be empty
# — meaning every package we listed is in the image, and vice versa.
diff \
<(jq -r '.packages[] | "\(.name)@\(.versionInfo)"' dashboard-sbom.spdx.json | sort -u) \
<(jq -r '.packages[] | "\(.name)@\(.versionInfo)"' /tmp/local-sbom.spdx.json | sort -u)
Expected: empty diff. A non-empty diff means either we added / removed something between SBOM-generation time and image-build time, or the image you pulled is not the one we shipped. Open a bug report if you see this.
Step 6 — (Optional) Scan the SBOM for known vulnerabilities¶
Trivy + Grype both accept SPDX SBOMs and tell you which packages have known CVEs.
# Grype
grype sbom:dashboard-sbom.spdx.json -o table
# Trivy
trivy sbom dashboard-sbom.spdx.json
If a CVE appears that you believe doesn't apply to your usage, file a VEX statement following the VEX policy.
Step 7 — (Optional) Verify the release-feed manifest¶
The release-feed is the signed channel the self-upgrade flow consumes (ADR 0013).
git clone --branch release-feed \
https://github.com/nollagluiz/AI_forSE.git \
/tmp/release-feed
cat /tmp/release-feed/v3.7.0/manifest.json | jq .
Each image entry has its digest (immutable) — verify the digest you pulled in Step 1 matches the digest in the manifest:
PULLED_DIGEST=$(docker inspect "${IMG}" --format='{{index .RepoDigests 0}}' | awk -F@ '{print $2}')
MANIFEST_DIGEST=$(jq -r '.images.dashboard.digest' /tmp/release-feed/v3.7.0/manifest.json)
[ "${PULLED_DIGEST}" = "${MANIFEST_DIGEST}" ] && echo "✅ Digest match" || echo "❌ DIGEST MISMATCH"
Step 8 — (Optional) Verify the Tier B obfuscation receipt¶
Tier B binaries are garble-obfuscated in CI. The release ships a SHA-256 of the tier-policy YAML so an auditor can confirm the build used the same partition policy as the public repo.
# What the release shipped:
gh release download "${TAG}" \
--repo nollagluiz/AI_forSE \
--pattern "tier-policy.yaml.sha256"
cat tier-policy.yaml.sha256
# What the current repo says:
sha256sum platform/ztp-prem/tier-policy.yaml
Both values must match.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
Cosign verify fails with no matching signatures |
You pulled :latest but the tag has been moved |
Re-pull by digest or by explicit version tag |
Cosign verify fails with Certificate not found |
Image was pushed but never signed (impossible for a real release) | Stop. Re-pull from a known-good source. |
gh attestation verify fails but Cosign succeeded |
Slim-SBOM attestation hit the 16 MiB API cap | Tolerated. Full SBOM on release page is authoritative. |
| Step 5 diff is non-empty | SBOM generation drift between CI and your local Syft version | Check Syft versions match. If they do, file a bug. |
| Step 7 digest mismatch | Image tag was re-pushed after release-feed manifest was signed | Stop. File a security advisory — this should never happen on a maintained release. |
What "✅ Verified" actually proves¶
After all 8 steps:
- The image you pulled was built by this GitHub Actions workflow in this repository, signed at tag time by a Sigstore certificate that GitHub vouches for
- The SBOM the release advertises matches the contents of the image you ran
- The image digest matches the release-feed manifest the self-upgrade flow consumes
- The Tier B partition that defined moat-closed code at build time was the same policy file that ships in the public repo
That's the supply-chain audit floor for any TLSStress.Art
release. The release-prep checklist in
docs/governance/RELEASE_CADENCE.md
ensures we produce every one of those artefacts before tagging.
Related¶
- ADR 0005 — why we sign + SBOM
- ADR 0013 — release-feed channel
docs/governance/RELEASE_CADENCE.md— release-prep checklistdocs/governance/QUALITY_GATES.md— release-attached artefact inventorydocs/governance/VEX_POLICY.md— filing a CVE-doesn't-apply statement
Last verified against shipping code: v3.7.0 (2026-05-12).