Every Shepherd release is signed by the workflow that built it, so you can check that what you install came from this repository’s release workflow, at the tag it claims, and was not altered afterwards:
checksums.txt lists their digests.shepherd and
shepherd-simulator) carry build provenance and an SPDX SBOM, both
stored in GitHub and pushed to the registry next to the image..github/allowed_signers; the release workflow refuses a tag that is not.The release workflow runs these same commands against everything it publishes before the
run turns green. You need the GitHub CLI for
gh attestation; the cosign alternatives need
cosign 3 or later.
Every gh attestation verify below pins three things: the repository
(-R), the workflow that signed (--signer-workflow) and the tag it
ran for (--source-ref). Drop none of them — without them, an attestation
from some other workflow would also pass.
The tag is the app version with a v; image tags drop the v; the
chart has its own version (Kubernetes (Helm)).
TAG=vX.Y.Z # the release tag
VERSION="${TAG#v}" # the image tag
CHART_VERSION=A.B.C # the chart version that release published
REPO=procoduck/shepherd
SIGNER="$REPO/.github/workflows/release.yml"
Download the archive and checksums.txt, check the digest, then check the
archive’s provenance:
gh release download "$TAG" -R "$REPO" -p checksums.txt -p "shepherd_${VERSION}_linux_amd64.tar.gz"
sha256sum --ignore-missing --check checksums.txt
gh attestation verify "shepherd_${VERSION}_linux_amd64.tar.gz" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"
Every file checksums.txt lists is attested, so the same command works for the
shepherd-mcp archives and the .sbom.json files. On macOS use
shasum -a 256 --ignore-missing --check checksums.txt.
By tag, or — better, since a tag can move — by the digest you actually deploy.
Repeat for shepherd-simulator.
IMAGE=ghcr.io/procoduck/shepherd
gh attestation verify "oci://$IMAGE:$VERSION" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"
# The digest the tag resolves to (or the one in your manifests)
DIGEST=$(docker buildx imagetools inspect "$IMAGE:$VERSION" --format '{{json .Manifest.Digest}}' | tr -d '"')
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"
helm pull prints the chart’s digest as it downloads it:
CHART=ghcr.io/procoduck/charts/shepherd
gh attestation verify "oci://$CHART:$CHART_VERSION" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"
CHART_DIGEST=$(helm pull "oci://$CHART" --version "$CHART_VERSION" 2>&1 | awk '$1 == "Digest:" { print $2 }')
gh attestation verify "oci://$CHART@$CHART_DIGEST" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"
Image and chart attestations are also stored in the registry beside the artifact, so a
mirror or an air-gapped cluster can verify them. Add --bundle-from-oci to any
image or chart command above:
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --bundle-from-oci
gh attestation verify "oci://$CHART@$CHART_DIGEST" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --bundle-from-oci
Or with cosign, which reads only the registry copy:
cosign verify-attestation --type slsaprovenance1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity "https://github.com/$SIGNER@refs/tags/$TAG" \
"$IMAGE@$DIGEST" > /dev/null
cosign verify-attestation --type slsaprovenance1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity "https://github.com/$SIGNER@refs/tags/$TAG" \
"$CHART@$CHART_DIGEST" > /dev/null
Both commands exit non-zero unless an attestation verifies; the redirect only hides the attestation payload cosign prints on success.
The SBOM attestation has the predicate type https://spdx.dev/Document/v2.3.
Verify it, then write the verified SPDX document to a file:
SPDX=https://spdx.dev/Document/v2.3
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --predicate-type "$SPDX"
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
--signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --predicate-type "$SPDX" \
--format json --jq '.[0].verificationResult.statement.predicate' > shepherd.spdx.json
The same SBOM, from the registry with cosign:
cosign verify-attestation --type "$SPDX" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity "https://github.com/$SIGNER@refs/tags/$TAG" \
"$IMAGE@$DIGEST" > /dev/null
The SBOM describes the image’s linux/amd64 member; the
linux/arm64 member is built from the same Dockerfile, Go modules and base
image.
Check the tag against the signers list on main, which only a reviewed pull
request can change:
git clone https://github.com/procoduck/shepherd.git && cd shepherd
git -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag "$TAG"
A good tag prints Good "git" signature for …. Tags from before signing
was enforced are unsigned and fail this check. Their archives and images still verify
through GitHub’s API as above; chart provenance, image SBOMs and the registry copies
start with the first release that has a signed tag.