How to Generate and Sign an SBOM for a Container Image With Syft and Cosign
A beginner-friendly, step-by-step tutorial that walks you through generating a Software Bill of Materials with Syft and cryptographically signing it with Cosign and Sigstore, so anyone downstream can...
In July 2026, Canonical published survey research based on 500 DevOps professionals showing that 48% believe verifiable, tracked packages would improve their confidence in software supply chain security, while 35% of organizations still rely on manual code review for security and 71% report real tension between DevOps and platform teams over how open source is managed at scale. The gap between “we should know what is in our software” and actually proving it is a tooling problem, and it already has a free, open-source answer: generate a Software Bill of Materials (SBOM) for every container image you ship, then cryptographically sign it so anyone downstream can verify exactly what is inside and who vouches for it.
Table Of Content
- What you will learn: SBOMs, signing, and chain of trust, in plain terms
- Prerequisites
- Step 1: Push a test image to a registry you control
- Step 2: Install Syft and list what is inside your image
- Step 3: Generate SPDX and CycloneDX SBOM files
- Step 4: Install Cosign and create a signing key pair
- Step 5: Sign the image by digest
- Step 6: Attach the SBOM as a signed attestation
- Step 7: Verify the signature and the attestation
- Common mistakes and how to catch them
- Verifying it works end-to-end
- Next steps
- Sources
By the end of this walkthrough, you will have pushed a container image to a registry, generated a machine-readable SBOM for it with Anchore’s Syft, signed the image and attached the SBOM as a verified attestation using Sigstore’s Cosign, and verified the entire chain end-to-end from a clean terminal. Everything here uses free, open-source command-line tools and is cross-checked against Anchore’s official Syft documentation, the Sigstore Cosign documentation, and Canonical’s chain of trust research.
What you will learn: SBOMs, signing, and chain of trust, in plain terms
A Software Bill of Materials (SBOM) is, in Anchore’s words, a detailed list of all the libraries and components that make up a piece of software. If your container image includes fifty operating system packages and a dozen application dependencies, an SBOM is the itemized receipt naming every one of them, by name and exact version. Without one, answering “are we affected by this newly disclosed vulnerability?” means guessing; with one, it means running a query.
An SBOM alone only tells you what a scan found. It does not tell you whether that list is genuine or whether the image has been swapped for something else since the SBOM was generated. That is what signing solves. Sigstore is an open-source project for signing, verifying, and recording the provenance of software artifacts, and Cosign is its command-line tool for signing and verifying container images. When you sign an image, you create a cryptographic proof, tied to a specific key, that a specific identity vouches for a specific, unchangeable copy of that image.
Combine the two and you get an attestation: a signed statement that binds a claim (in our case, “here is the SBOM for this exact image”) to that image’s cryptographic identity. This is the mechanism behind the phrase “chain of trust”, not one certificate, but a series of verifiable links: this exact image maps to this exact package list, and this exact signer vouches for that mapping. That is precisely the gap Canonical’s research identifies: most organizations have vulnerability scanners, but far fewer have a verifiable, machine-checkable record of what shipped and who approved it.
Prerequisites
- Operating system: Ubuntu 24.04 LTS or 26.04 LTS is used for the commands below, but every tool here is a portable binary, so the same steps work on any Linux distribution, and Cosign and Syft both also ship macOS and Windows builds.
- Docker: installed and running, with a working
dockercommand. Confirm withdocker --version. - A free Docker Hub account: sign up at hub.docker.com if you do not already have one. Signing and attesting attach data to an image reference that lives in a real registry, so you need somewhere you can push to.
- jq: a command-line JSON processor used to inspect SBOM output. Install it with
sudo apt install jqon Ubuntu/Debian. - Prior knowledge assumed: comfort running commands in a terminal and basic familiarity with Docker (pulling and pushing images). No prior experience with SBOMs, Sigstore, Cosign, or Syft is assumed: each concept is explained the first time it appears.
Step 1: Push a test image to a registry you control
Cosign signs and attests a specific image reference stored in a registry, so the very first thing you need is an image you can push to. We will use the small, public alpine image as our test subject, re-tagged into a repository under your own Docker Hub username.
# Context: Ubuntu 24.04 LTS or newer, Docker installed and running.
# Purpose: pull a small public image and push a copy into a registry path you control.
docker pull alpine:latest
docker login
docker tag alpine:latest docker.io/<dockerhub-username>/sbom-demo:latest
docker push docker.io/<dockerhub-username>/sbom-demo:latest
Expected output: docker login asks for your Docker Hub username and password (or access token) and prints Login Succeeded. docker push uploads each image layer and finishes with a line similar to:
latest: digest: sha256:0a1b2c3d4e5f... size: 1234
That sha256:... value is the image’s digest, a cryptographic hash of its exact contents. You can fetch it again at any time with:
# Context: same machine, image already pushed.
# Purpose: look up the exact digest of the image you just pushed.
docker inspect --format='{{index .RepoDigests 0}}' docker.io/<dockerhub-username>/sbom-demo:latest
Expected output: docker.io/<dockerhub-username>/sbom-demo@sha256:0a1b2c3d4e5f...
Why the digest matters: a tag like :latest is a mutable pointer that can be repointed at completely different content tomorrow. A digest identifies the content itself and cannot be reused for anything else. Every signing, attesting, and verifying command in the rest of this tutorial targets the image by its @sha256:... digest, never by tag: keep the digest string handy, since you will reuse it in every step from here on.
Step 2: Install Syft and list what is inside your image
Syft is Anchore’s open-source CLI tool for generating an SBOM from container images and filesystems. Install the latest release with the official installer script:
# Context: Ubuntu 24.04 LTS or newer, sudo access.
# Purpose: download and install the latest Syft release to /usr/local/bin.
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
syft version
Expected output: the install script prints where it placed the binary, and syft version prints a version string confirming Syft is on your PATH.
Now point Syft at the image you pushed, using its digest:
# Context: image already pushed to Docker Hub in Step 1.
# Purpose: list every package Syft finds inside the image.
syft docker.io/<dockerhub-username>/sbom-demo@sha256:<your-digest>
Expected output: a human-readable table with NAME, VERSION, and TYPE columns, one row per package Syft found: for a plain Alpine base image this typically includes entries like alpine-baselayout, busybox, and apk-tools, each tagged as an apk package.
What just happened? Syft pulled the image’s layers, ran its built-in catalogers against them (Alpine’s apk database, Debian’s dpkg, language package managers, and dozens more depending on what it finds), and printed every installed package it could positively identify. By default it only reports what is visible in the final, squashed image; add --scope all-layers to also see packages that were installed and later removed in an earlier layer, often a useful thing to audit in its own right.
Step 3: Generate SPDX and CycloneDX SBOM files
A table on your screen is not something a scanner or a signing tool can consume. Ask Syft for machine-readable output in the two dominant industry-standard SBOM formats: SPDX (maintained under the Linux Foundation) and CycloneDX (maintained by OWASP).
# Context: same image reference as Step 2.
# Purpose: write the SBOM to disk in both SPDX and CycloneDX JSON formats.
syft docker.io/<dockerhub-username>/sbom-demo@sha256:<your-digest> \
-o table \
-o spdx-json=sbom.spdx.json \
-o cyclonedx-json=sbom.cdx.json
Expected output: the same package table prints to your terminal, and two new files appear in your working directory: sbom.spdx.json and sbom.cdx.json.
Confirm the files actually contain what you expect using jq:
# Context: sbom.spdx.json and sbom.cdx.json created above.
# Purpose: list every package name each SBOM file records.
jq '.packages[].name' sbom.spdx.json
jq '.components[].name' sbom.cdx.json
Expected output: a quoted, newline-separated list of package names, identical in substance between the two files: they describe the same image, just structured differently (SPDX nests data under a top-level packages array, CycloneDX under components).
We will carry sbom.spdx.json forward into the signing steps, because Cosign has a built-in predicate type that maps directly onto SPDX JSON, which keeps the next commands simple.
Step 4: Install Cosign and create a signing key pair
Cosign is Sigstore’s command-line tool for signing and verifying container images and other artifacts. Install the latest release binary:
# Context: Ubuntu 24.04 LTS or newer, sudo access.
# Purpose: download and install the latest Cosign release to /usr/local/bin.
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign
cosign version
Expected output: cosign version prints version and build information, confirming the binary is installed and executable.
Cosign supports two ways of signing: keyless signing, where you authenticate with an OpenID Connect identity (Google, GitHub, or Microsoft) and Cosign requests a short-lived certificate from Sigstore’s public certificate authority, Fulcio, for each signature; and key-based signing, where you generate your own long-lived key pair and use it directly. Keyless signing is the better fit for CI pipelines, since there is no private key to store as a secret, but it requires an interactive OIDC login, which adds a moving part to a first walkthrough. This tutorial uses key-based signing so every command is fully reproducible offline; the “Next steps” section below links to keyless signing once you are comfortable with the basics.
# Context: same machine as above.
# Purpose: generate a Cosign key pair for signing.
cosign generate-key-pair
Expected output: Cosign prompts you to enter (and confirm) a password, then writes two files to the current directory: cosign.key (an encrypted private key) and cosign.pub (the corresponding public key).
This is the single most important file in this tutorial to handle carefully: cosign.key plus its password together let anyone sign artifacts as you. Treat it exactly like an SSH private key: never commit it to version control, and store a backup somewhere access-controlled. cosign.pub, by contrast, is meant to be shared; anyone who wants to verify your signatures needs it.
Step 5: Sign the image by digest
# Context: cosign.key created in Step 4, image pushed in Step 1.
# Purpose: cryptographically sign the image, identified by its digest.
cosign sign --key cosign.key --yes docker.io/<dockerhub-username>/sbom-demo@sha256:<your-digest>
Expected output: a prompt for the cosign.key password, followed by a line confirming the signature was pushed to the registry, similar to Pushing signature to: docker.io/<dockerhub-username>/sbom-demo:sha256-<digest>.sig.
The --yes flag skips an interactive confirmation prompt that Cosign shows by default, warning you that the signature (and, by default, the fact that you made it) will be recorded in Sigstore’s public Rekor transparency log, a tamper-evident, publicly auditable ledger. That is worth pausing on: even with your own private key, Cosign timestamps the signing event publicly by default, so anyone can later confirm a signature existed at a given time, independent of trusting your key alone. Omit --yes the first time you run this if you want to see that prompt for yourself.
Cosign’s own documentation is explicit about why we have been signing by digest rather than by tag this whole time: “Make sure to sign the image by its digest (@sha256:…) rather than by tag (:latest) so that you actually sign what you think you’re signing! This prevents race conditions or (worse) malicious tampering.” A tag can be silently repointed between the moment you decide to sign and the moment the signature is created; a digest cannot.
Step 6: Attach the SBOM as a signed attestation
A signature on the image proves the image itself is untampered and identifies who signed it. An attestation goes further: it binds a specific, structured claim (your SBOM) to that image, and signs the whole package together, using the in-toto attestation format. Downstream, anyone can ask “what packages does this image contain, and is that claim itself signed?” instead of trusting an unsigned file sitting next to the image.
# Context: sbom.spdx.json from Step 3, cosign.key from Step 4.
# Purpose: attach the SBOM to the image as a signed, verifiable attestation.
cosign attest --predicate sbom.spdx.json --type spdxjson --key cosign.key --yes \
docker.io/<dockerhub-username>/sbom-demo@sha256:<your-digest>
Expected output: another password prompt, then confirmation that the attestation was pushed to the registry alongside the image and its signature.
Watch the spelling here: Syft’s output flag is spdx-json (with a hyphen), but Cosign’s predicate type flag is --type spdxjson (no hyphen). They are two different tools’ naming conventions for the same standard, and mixing them up is an easy, silent mistake: Cosign accepts slsaprovenance, link, spdx, spdxjson, cyclonedx, vuln, openvex, or custom as its built-in --type values, per the official cosign attest reference.
Step 7: Verify the signature and the attestation
This is the step a downstream consumer, a CI gate, or a future version of you actually runs before trusting the image:
# Context: cosign.pub from Step 4, same image digest throughout.
# Purpose: verify the image signature and the SBOM attestation independently.
cosign verify --key cosign.pub docker.io/<dockerhub-username>/sbom-demo@sha256:<your-digest>
cosign verify-attestation --key cosign.pub docker.io/<dockerhub-username>/sbom-demo@sha256:<your-digest>
Expected output: cosign verify prints a JSON array describing the checks it performed (confirming the signature matches the public key and the image digest) followed by the signature payload. cosign verify-attestation does the same for the attestation, and its payload contains your signed SBOM predicate. Both commands exit with status code 0 on success: check with echo $? immediately after running one, if you want to confirm this in a script rather than by reading output.
Common mistakes and how to catch them
- Signing or verifying by tag instead of digest. If you run any of these commands against
:latestinstead of@sha256:..., you are trusting whatever the tag happens to point to right now, which defeats the purpose. Always resolve and reuse the digest from Step 1. - Losing the
cosign.keypassword. There is no recovery mechanism: a forgotten password means the key is useless and you must generate a new pair and re-sign everything with it. Store the password in a password manager the moment you create it. - Confusing
spdx-json(Syft’s output flag) withspdxjson(Cosign’s predicate type). Both are valid, unrelated flags in two different tools; typing the wrong one produces a confusing error rather than a helpful one. - Trying to sign an image that was never pushed. Cosign needs to write the signature and attestation into a real registry alongside the image; a purely local
docker buildoutput with no corresponding registry copy has nothing for Cosign to attach to. - Committing
cosign.keyto a Git repository. Add it to.gitignoreimmediately after generating it, exactly as you would an SSH private key or a.envfile full of secrets. - Being surprised by the Rekor confirmation prompt. If you forget
--yes,cosign signandcosign attestwill pause and ask you to confirm that the event will be recorded in the public transparency log before continuing: this is expected behavior, not a hang.
Verifying it works end-to-end
Before you rely on this in a real pipeline, work through this checklist:
- Run
syft <image>@<digest> -o tableagain and confirm the package list still matches what you expect for the base image you chose. - Run
cosign verify --key cosign.pub <image>@<digest>and confirm it exits with status0and prints the expected signature checks. - Run
cosign verify-attestation --key cosign.pub <image>@<digest>and confirm it exits0and the printed payload namesspdxjsonas its predicate type, that is Cosign confirming the attached, signed claim is the SBOM you attested in Step 6, not just any signed statement. - As a negative test, run
cosign verify --key cosign.pubagainst a different image reference, or generate a second, throwaway key pair and try verifying with the wrong public key. Confirm Cosign refuses and exits non-zero, proving the check actually discriminates between valid and invalid signatures, rather than passing everything.
Next steps
Once this manual walkthrough makes sense, the natural next moves are: switching from key-based to keyless signing in CI, using the OIDC identity GitHub Actions or GitLab already provides so there is no private key to manage as a secret; enforcing verification automatically at deploy time in Kubernetes with Sigstore’s policy-controller or a Kyverno policy, so unsigned or unattested images are rejected by the cluster rather than by a human remembering to check; and generating SLSA provenance attestations (cosign attest --type slsaprovenance1) alongside your SBOM, which additionally records how and where the image was built. Each of these is exactly the kind of “verifiable provenance” investment Canonical’s July 2026 research found DevOps teams increasingly want, but have not yet automated.
Sources
- Canonical / Ubuntu Blog: Building an Open Source Chain of Trust: New Research Uncovers Key Blockers and Ways Forward
- Anchore Open Source: Syft Getting Started Guide
- Anchore Syft: GitHub Repository and README
- Sigstore Documentation: Cosign Installation
- Sigstore Documentation: Signing Containers
- Sigstore Documentation: Verifying Signatures
- Sigstore Cosign:
cosign attestCLI Reference - Featured image source: Metal chain, Wikimedia Commons
Image credit: JJ Ying, CC0 1.0 Universal (Public Domain Dedication) via Wikimedia Commons; cropped, resized, and converted to WebP for sxz.io.








No Comment! Be the first one.