Skip to content

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):

ArtifactProduced byPredicate / type
Signaturecosign sign (keyless)https://sigstore.dev/cosign/sign/v1
SBOMactions/attest (Trivy SPDX)https://spdx.dev/Document/v2.3
SLSA provenanceactions/attest-build-provenancehttps://slsa.dev/provenance/v1
Mirror metadataactions/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 means gh attestation verify works out of the box against the GitHub API, and --bundle-from-oci is available as an offline alternative that reads the bundle straight from the registry. The create-storage-record: false setting 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:

json
{
  "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

bash
brew install cosign gh
# or download from the cosign / GitHub CLI release pages

Pick an image to verify:

bash
IMAGE=ghcr.io/this-is-tobi/mirror/mattermost:latest

Signature

bash
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

bash
gh attestation verify "oci://$IMAGE" --owner this-is-tobi
# default --predicate-type is https://slsa.dev/provenance/v1

SBOM

bash
gh attestation verify "oci://$IMAGE" \
  --owner this-is-tobi \
  --predicate-type "https://spdx.dev/Document/v2.3"

Extract the package list:

bash
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

bash
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:

bash
gh attestation verify "oci://$IMAGE" \
  --owner this-is-tobi \
  --bundle-from-oci

Stricter scoping (optional)

To assert both the source repository and the signing workflow explicitly:

bash
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:

  1. Signs the digest with cosign sign (keyless).
  2. Generates and attaches an SBOM — Trivy produces SPDX, attached via actions/attest.
  3. Attaches standard SLSA build provenance via actions/attest-build-provenance.
  4. Attaches the mirror predicate (upstream source/version + architectures) via actions/attest with 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:

yaml
permissions:
  contents: read # read repository content
  packages: write # push image + attestations to GHCR
  id-token: write # OIDC token for keyless signing
  attestations: write # create attestations

Trust model

Verifying an image establishes that:

  1. It was built by the shared CI workflow (this-is-tobi/github-workflows/.github/workflows/attest-docker.yml) on behalf of this-is-tobi/multiarch-mirror.
  2. Its exact contents (SBOM) and build origin (SLSA provenance) are as attested.
  3. 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

  1. Match the exact --predicate-type (e.g. https://spdx.dev/Document/v2.3, not https://spdx.dev/Document).
  2. Scope with --owner this-is-tobi (or --signer-repo this-is-tobi/github-workflows), since the signer is the reusable workflow.
  3. If the GitHub API is unreachable (offline, air-gapped), add --bundle-from-oci to 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.

References