Skip to main content

Every release image is signed by the Rolter release workflow, and each platform image carries an SBOM. You can check both before you deploy, without trusting the registry or whoever holds a push token for it.

WhatAttached toMade byChecked with
SignatureThe multi-arch manifest list and each platform imagecosign, keyless (Sigstore)cosign verify
SBOM (CycloneDX)The linux/amd64 and linux/arm64 imagessyft, signed as an attestation by cosigncosign verify-attestation
SLSA build provenanceThe multi-arch manifest listGitHub artifact attestationsgh attestation verify

The signatures are keyless, so there is no Rolter public key to download. Each one carries a short-lived certificate from Sigstore’s Fulcio that names the GitHub Actions workflow that signed it, and the signature is recorded in Sigstore’s public transparency log. Verifying means checking that certificate names Rolter’s release workflow.

This covers images from the first release that includes this page. Images published before it are unsigned.

Verify an image#

The repository ships the check as one script, scripts/verify-image.sh. The release workflow runs this same script against every image right after signing it, so if a release went out, this check passed against it.

You need cosign 3 or later, jq, and either docker with buildx or crane. The provenance check also needs the GitHub CLI, signed in with gh auth login or given a GH_TOKEN, because it reads GitHub’s attestation API.

A passing run ends with verified ghcr.io/rolter-ai/rolter:1.0.0:

Any failure exits non-zero before the last line. Without the GitHub CLI, pass --skip-provenance to check the signatures and SBOMs only.

Verify by digest rather than by tag if you deploy by digest: bash verify-image.sh ghcr.io/rolter-ai/rolter@sha256:<digest>. A tag can be moved later, and a digest cannot.

Note

The script fetches from the tag you name, so it checks against the rules that release was published under. Read it before you run it. It is short, and every check it makes is listed below.

What the script checks#

If you would rather run the commands yourself, or write them into an admission policy, these are the values the script uses.

FieldValue
OIDC issuerhttps://token.actions.githubusercontent.com
Certificate identityhttps://github.com/rolter-ai/rolter/.github/workflows/release.yml@refs/heads/master, or @refs/tags/v<version>
Workflow repositoryrolter-ai/rolter
SBOM predicate typecyclonedx (https://cyclonedx.org/bom)

As one regular expression, which is what the script passes to cosign:

Releases are dispatched from master, so the certificate of a normal release names refs/heads/master. A release started by pushing a v* tag names that tag. A signature from any other branch, workflow or repository fails the check.

The signature check, for the manifest list and again for each platform image:

The build provenance check:

Read the SBOM#

The SBOM is attached to each platform image, not to the multi-arch tag, because each architecture is a different image. Pick the platform digest from the manifest list, then verify and extract the attestation:

cosign verify-attestation prints one signed envelope per line, and a re-run of the signing job can leave more than one, so the command keeps the first.

The result is a CycloneDX JSON document you can hand to a scanner, for example grype sbom:rolter-sbom.cdx.json or trivy sbom rolter-sbom.cdx.json. The release workflow also uploads the same files as the sbom artifact of its run.

The SBOM is two scans merged into one document:

  • The image layers: the Debian packages of the distroless base image and the files they own, and the Rust crates the three rolter binaries link. The binaries are built with cargo auditable, which embeds their exact dependency list, so the crates are read from the binaries and not from Cargo.lock.
  • The dashboard’s lockfile: every package in the release tag’s ui/bun.lock. The dashboard bundle does not carry its own dependency list, so an image scan alone would miss it.

The Rust list is exact. The dashboard list still contains more than the image runs, because the bun.lock scan includes the dashboard’s build tooling: the scanner cannot split production from development packages reliably, and an SBOM that drops shipped packages is worse than one that lists too many. A scanner finding in a development-only npm package may not reach the running image.

Docker Hub#

When a release is also mirrored to Docker Hub, the Docker Hub copy is signed and attested separately, with the same identity. A signature stored in GHCR does not cover an image pulled from docker.io. Pass the Docker Hub reference to the same script:

Enforce it in Kubernetes#

An admission controller can refuse unsigned images using the issuer and identity above. With the Sigstore policy-controller, the keyless authority is:

Kyverno’s verifyImages rule takes the same issuer and subject. Check that your controller reads Sigstore bundles stored as OCI referrers, the format cosign 3 writes.