Image Attestations and Signatures
This document explains the supply-chain security metadata attached to every image built by this project, and how to verify it.
Overview
Each mirrored image carries four independent pieces of supply-chain metadata, attached to the image digest in GitHub Container Registry (GHCR):
| Artifact | Produced by | Predicate / type |
|---|---|---|
| Signature | cosign sign (keyless) | https://sigstore.dev/cosign/sign/v1 |
| SBOM | actions/attest (Trivy SPDX) | https://spdx.dev/Document/v2.3 |
| SLSA provenance | actions/attest-build-provenance | https://slsa.dev/provenance/v1 |
| Mirror metadata | actions/attest (custom predicate) | https://this-is-tobi.github.io/multiarch-mirror/mirror/v1 |
All four are generated by the shared reusable workflow attest-docker.yml called from each application's attest job.
Where the attestations live. The SBOM, provenance and mirror attestations are stored in two places: GitHub's attestations database (always, via the attest actions) and GHCR as OCI artifacts (
push-to-registry: true). This meansgh attestation verifyworks out of the box against the GitHub API, and--bundle-from-ociis available as an offline alternative that reads the bundle straight from the registry. Thecreate-storage-record: falsesetting only skips GitHub's artifact metadata storage record (a linked-artifacts feature restricted to organization-owned repositories) — it does not affect attestation storage.
What each attestation contains
Signature (cosign)
Keyless signature proving the image digest was produced by this project's CI. No long-lived keys — identity comes from GitHub OIDC, certificates from Fulcio, transparency from Rekor.
SBOM
An SPDX Software Bill of Materials generated by Trivy: every package/dependency in the image with version information, for vulnerability scanning and supply-chain transparency.
SLSA provenance
GitHub's standard SLSA v1 build provenance (buildType https://actions.github.io/buildtypes/workflow/v1), auto-generated from the Actions run context — the building workflow, repository, commit and run. Verifiable by slsa-verifier and gh attestation verify.
Mirror metadata (custom predicate)
Records which upstream artifact this image mirrors — information the auto-generated provenance has no field for:
{
"mirror": {
"registry": "ghcr.io",
"namespace": "this-is-tobi/mirror",
"image": "mattermost",
"architectures": ["amd64", "arm64"],
"multiArch": true,
"buildMode": "multi-version"
},
"upstream": {
"repository": "mattermost/mattermost",
"source": "https://github.com/mattermost/mattermost",
"version": "10.5.0",
"ref": "v10.5.0"
}
}Identities
Signing and attesting run inside the shared reusable workflow, so the signing certificate identity (SAN) is that workflow, while the source repository is this repo:
- Signer identity (SAN):
https://github.com/this-is-tobi/github-workflows/.github/workflows/attest-docker.yml@refs/tags/v0 - Source repository:
https://github.com/this-is-tobi/multiarch-mirror - OIDC issuer:
https://token.actions.githubusercontent.com
This is why verification scopes to --owner this-is-tobi (or --signer-repo this-is-tobi/github-workflows) rather than --repo this-is-tobi/multiarch-mirror alone.
Verifying images
Prerequisites
brew install cosign gh
# or download from the cosign / GitHub CLI release pagesPick an image to verify:
IMAGE=ghcr.io/this-is-tobi/mirror/mattermost:latestSignature
cosign verify \
--certificate-identity-regexp "^https://github.com/this-is-tobi/github-workflows/\.github/workflows/attest-docker\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE"SLSA provenance
gh attestation verify "oci://$IMAGE" --owner this-is-tobi
# default --predicate-type is https://slsa.dev/provenance/v1SBOM
gh attestation verify "oci://$IMAGE" \
--owner this-is-tobi \
--predicate-type "https://spdx.dev/Document/v2.3"Extract the package list:
gh attestation verify "oci://$IMAGE" \
--owner this-is-tobi \
--predicate-type "https://spdx.dev/Document/v2.3" --format json \
| jq -r '.[0].verificationResult.statement.predicate.packages[] | "\(.name) \(.versionInfo // "-")"'Mirror metadata
gh attestation verify "oci://$IMAGE" \
--owner this-is-tobi \
--predicate-type "https://this-is-tobi.github.io/multiarch-mirror/mirror/v1" --format json \
| jq '.[0].verificationResult.statement.predicate'Offline / registry-only verification (optional)
Every attestation bundle is also pushed to GHCR, so verification works without access to the GitHub API — useful in air-gapped environments or registry-only policy engines. Add --bundle-from-oci to any of the commands above:
gh attestation verify "oci://$IMAGE" \
--owner this-is-tobi \
--bundle-from-ociStricter scoping (optional)
To assert both the source repository and the signing workflow explicitly:
gh attestation verify "oci://$IMAGE" \
--repo this-is-tobi/multiarch-mirror \
--signer-repo this-is-tobi/github-workflows \
--predicate-type "https://slsa.dev/provenance/v1"Implementation
Attestations are produced by the reusable attest-docker.yml workflow, called from each application's attest job after the image manifest is pushed. For each image it:
- Signs the digest with
cosign sign(keyless). - Generates and attaches an SBOM — Trivy produces SPDX, attached via
actions/attest. - Attaches standard SLSA build provenance via
actions/attest-build-provenance. - Attaches the mirror predicate (upstream source/version + architectures) via
actions/attestwith a custom predicate type.
Callers pass SIGN: true, SBOM: true, PROVENANCE: true, and the PREDICATE_TYPE/PREDICATE inputs carrying the mirror metadata.
Permissions
The attest job requires:
permissions:
contents: read # read repository content
packages: write # push image + attestations to GHCR
id-token: write # OIDC token for keyless signing
attestations: write # create attestationsTrust model
Verifying an image establishes that:
- It was built by the shared CI workflow (
this-is-tobi/github-workflows/.github/workflows/attest-docker.yml) on behalf ofthis-is-tobi/multiarch-mirror. - Its exact contents (SBOM) and build origin (SLSA provenance) are as attested.
- It mirrors the stated upstream release (mirror predicate).
All of this is anchored in GitHub OIDC (token.actions.githubusercontent.com), Fulcio (certificate authority) and Rekor (public transparency log) — with no long-lived secrets to manage.
Troubleshooting
cosign verify fails on identity
The signer is the shared reusable workflow, not this repository. Use the github-workflows/.github/workflows/attest-docker.yml@… identity shown under Identities, not a multiarch-mirror identity.
gh attestation verify finds no attestations
- Match the exact
--predicate-type(e.g.https://spdx.dev/Document/v2.3, nothttps://spdx.dev/Document). - Scope with
--owner this-is-tobi(or--signer-repo this-is-tobi/github-workflows), since the signer is the reusable workflow. - If the GitHub API is unreachable (offline, air-gapped), add
--bundle-from-ocito read the bundle directly from GHCR instead.
Attestation not found on an older image
Attestations bind to a specific image digest. Images built before this attestation system was introduced may lack some attestations; always verify by digest or a current tag.