The vocabulary around build artefacts has grown faster than most teams can keep up with. SBOMs, attestations, provenance, signatures, SLSA levels. They sound like one big compliance project. In practice they are three small, separate answers to three questions you already ask during an incident.
This guide explains each in plain words and shows the commands to produce and check them for a container image.
Three questions
| Artefact | Question it answers | Open-source tool |
|---|---|---|
| SBOM | What is inside this image? | Syft |
| Signature | Is this the exact thing we published? | cosign |
| Provenance | Which source, workflow and builder produced it? | SLSA generators, cosign, GitHub attestations |
They work best together, attached to the image by digest so they cannot drift apart. A tag such as :latest can move; a digest is a hash of the content and cannot.
SBOM: the ingredients list
A software bill of materials lists the packages and versions inside an artefact. It is useful the day a new vulnerability lands and someone asks which services ship the affected library. Without SBOMs, that is a week of grepping. With them, it is a query.
syft ghcr.io/acme/api@sha256:<digest> -o spdx-json > sbom.spdx.json
# scan the SBOM instead of the image
grype sbom:./sbom.spdx.json
Generate the SBOM in the same pipeline that builds the image, not later from a copy pulled out of production. SPDX and CycloneDX are both fine; pick one and stick with it.
Two limits are worth knowing. An SBOM is only as good as the tool's view of the image: binaries copied in by hand, or code vendored without a manifest, may not appear. And an SBOM is a snapshot. It says what was inside when it was built, which is exactly what you want, but it does not update itself, so store it next to the image rather than regenerating it later from something that might have changed.
Signatures: the tamper seal
A signature proves that a specific digest was signed by a specific identity. With Sigstore's keyless mode, that identity is your CI workflow, proved through OIDC, so there is no long-lived private key to steal or rotate.
# in CI, with id-token: write
cosign sign --yes ghcr.io/acme/api@sha256:<digest>
# anywhere else
cosign verify ghcr.io/acme/api@sha256:<digest> \
--certificate-identity-regexp '^https://github.com/acme/api/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
The verify step is the important half. Checking only that a signature exists proves little; check that it came from your repository and your workflow.
Provenance: the build receipt
Provenance is a signed statement describing how an artefact was built: the source repository and commit, the workflow, the builder and the inputs. It is what lets you say that the image in production came from the main branch of the right repository, built by CI rather than on someone's laptop.
You can attach the SBOM and provenance as attestations, signed in the same way as the image:
cosign attest --yes --type spdxjson \
--predicate sbom.spdx.json \
ghcr.io/acme/api@sha256:<digest>
# GitHub-native provenance, verified with the gh CLI
gh attestation verify oci://ghcr.io/acme/api@sha256:<digest> --repo acme/api
SLSA levels, briefly
SLSA is a framework for how much you can trust provenance. The build track has levels that build on each other:
- Level 1. Provenance exists and describes how the artefact was built. It catches mistakes but is easy to forge.
- Level 2. The build runs on a hosted platform that generates and signs the provenance itself.
- Level 3. The build platform is hardened: builds are isolated from each other and the signing material is out of reach of the build steps.
The levels describe the build, not the code. A Level 3 artefact can still contain a vulnerable library or a bug; what the level tells you is that the artefact really came from the source and process the provenance claims. That is why it pairs with the SBOM rather than replacing it.
Most teams on a hosted CI service can reach Level 2 quickly. Level 3 depends mainly on how the build platform is set up, and the SLSA project's reusable workflows exist to make it easier.
Where to start
- Pick one service with a container image and a CI pipeline.
- Build, push, and capture the digest from the push step.
- Generate the SBOM with Syft and attach it as an attestation.
- Sign the digest keyless with cosign.
- Add a verify step before deploy, pinned to your repository identity. Start it in warn mode, then make it block.
Once one service works, the rest is copying a reusable workflow. The value appears the first time someone asks what is running and where it came from, and the answer takes a minute rather than a meeting.