SBOMs and signing
Generate SBOMs with Syft, sign images with Cosign, and verify the artifacts that ship with stock Hummingbird images.
This section is about the artifacts that travel alongside an image without being part of its filesystem: software bills of materials (SBOMs), build provenance attestations, and Sigstore-style signatures. By the end, you’ll have:
- generated an SBOM for one of the images you built in section 4,
- signed that image with a local Cosign key pair,
- attached the SBOM as a Cosign attestation,
- verified your own signature, and
- verified the SBOM that ships with a stock Hummingbird image.
What’s actually attached to an image
Three distinct artifacts can be attached to an OCI image manifest:
- The signature. A short attestation that says “I, the signer, vouch for the bytes of this image.” Verifying a signature tells you the image hasn’t been tampered with since the signer signed it; it does not tell you anything about the contents.
- The SBOM attestation. A signed envelope wrapping a software bill of materials (SPDX or CycloneDX). The signature on this attestation is what makes the SBOM trustworthy.
- The provenance attestation (SLSA). A signed envelope describing how the image was built — what source it came from, what build system produced it, when, and with what inputs.
All three are stored in the registry as separate artifacts on the same manifest, addressable by the image digest.
Step 1 — Generate a Cosign key pair
Cosign supports both keyless OIDC signing (used in CI, where the identity comes from a GitHub Actions or GitLab token) and key-based signing (used in local development). We’ll use the key-based path here because it works without any external identity provider.
mkdir -p ~/.config/containers/signing
cd ~/.config/containers/signing
# Cosign will prompt for a passphrase. Use something memorable —
# you'll type it every time you sign during this section.
cosign generate-key-pair
You now have:
cosign.key— the encrypted private key (do not commit).cosign.pub— the public key (safe to share; goes into your policy.json, distributed to verifiers, etc.).
Step 2 — Sign one of your built images
Pick whichever of the section 4 images you have handy. The Node example is the smallest and fastest to push around:
# Tag the image into a registry namespace you can push to. Replace
# 'quay.io/your-username' with your own Quay namespace, or use a
# local registry if you prefer not to publish.
TARGET="quay.io/${USER}/hummingbird-node-example:latest"
podman tag hummingbird-node-example:latest "$TARGET"
podman push "$TARGET"
# Sign the published image. The --key flag tells Cosign to use the
# local key pair instead of OIDC.
cosign sign \
--key ~/.config/containers/signing/cosign.key \
--yes \
"$TARGET"
The --yes flag accepts the consent prompt about uploading the
signature to the public Sigstore transparency log. If you’re
working on something that should not appear in the public log, see
the Cosign docs on private TUF roots — that’s beyond the scope
of this tutorial.
Step 3 — Verify your signature
cosign verify \
--key ~/.config/containers/signing/cosign.pub \
"$TARGET" \
| jq
Expected: a JSON object describing the signature, the certificate
(if any), and the transparency-log entry. If cosign verify
exits non-zero, the image and signature are out of sync —
re-sign and try again.
Step 4 — Generate an SBOM with Syft
Syft can produce SBOMs in several formats. SPDX-JSON is the right default — it’s the format Cosign expects when attaching an SBOM as an SPDX attestation, and it’s the format most downstream tools prefer.
# Generate the SBOM. The output file is whatever path you want;
# we'll point Cosign at it in the next step.
syft "$TARGET" -o spdx-json="/tmp/sbom-node.spdx.json"
# Look at what's in there. The package count is the headline
# number — for a Hummingbird-based image it should be very small.
echo "SBOM package count:"
jq '.packages | length' /tmp/sbom-node.spdx.json
echo
echo "First few packages:"
jq '.packages[0:5] | .[] | {name, versionInfo, supplier}' \
/tmp/sbom-node.spdx.json
Step 5 — Attach the SBOM as a Cosign attestation
cosign attest \
--key ~/.config/containers/signing/cosign.key \
--predicate /tmp/sbom-node.spdx.json \
--type spdxjson \
--yes \
"$TARGET"
Now there are two artifacts attached to the manifest: the signature you created in step 2 and this SBOM attestation. You can verify both:
cosign verify-attestation \
--key ~/.config/containers/signing/cosign.pub \
--type spdxjson \
"$TARGET" \
| jq '.payload | @base64d | fromjson | .predicate.packages | length'
The output should match the package count from step 4 — proof that the SBOM you generated is the SBOM attached to the image.
Step 6 — Verify the SBOM that ships with a Hummingbird image
Hummingbird images ship with a Red Hat-signed SBOM by default.
The verification flow is the same as above, but the key/identity
used by the verifier is Red Hat’s, not yours. The flow below
uses cosign verify-attestation with the Sigstore public-key
flow to extract the SBOM that came pre-attached.
# Pick any Hummingbird image — Nginx will do.
HUMMINGBIRD_IMG="$HB_REGISTRY/nginx:1"
# Verify the Red Hat-signed SBOM attestation with Red Hat's public
# key and list the package names. (Use the signed registry path,
# registry.access.redhat.com/hi/, for verification — the
# quay.io/hummingbird mirror is unsigned.)
cosign verify-attestation \
--key "https://security.access.redhat.com/data/63405576.txt" \
--insecure-ignore-tlog \
--type spdxjson \
"$HUMMINGBIRD_IMG" \
| jq -r '.payload | @base64d | fromjson | .predicate.packages[].name' \
| head -20
The exact key and verification flags can change as the project’s signing infrastructure evolves — Red Hat publishes the canonical command in the Hummingbird catalog docs (
images.redhat.com) and the “Verifying Reproducibility” guide. The public key above is Red Hat’s current image-signing key. If verification errors, re-check the key/identity against those docs.
These images also carry a signed SLSA provenance attestation
(currently the https://slsa.dev/provenance/v0.2 predicate). You
can list every predicate type attached without guessing a --type:
cosign download attestation "$HUMMINGBIRD_IMG" \
| jq -rs '.[] | (.payload | @base64d | fromjson | .predicateType)' \
| sort -u
Note that cosign verify-attestation --type slsaprovenance targets
the v1.0 predicate URI, so it will not match the v0.2 attestation
on these images — enumerating the types as above avoids that trap.
Step 7 — A policy.json that enforces signatures
Once you’ve practiced signing and verifying manually, you can
make Podman / Buildah enforce the same checks automatically by
configuring ~/.config/containers/policy.json. This is the
file Podman consults before pulling or running any image.
A starter policy that requires Sigstore signatures for the Hummingbird namespace, while leaving everything else permissive:
cat > ~/.config/containers/policy.json <<EOF
{
"default": [
{ "type": "insecureAcceptAnything" }
],
"transports": {
"docker": {
"${HB_REGISTRY}": [
{
"type": "sigstoreSigned",
"keyPath": "$HOME/.config/containers/signing/cosign.pub",
"signedIdentity": { "type": "matchRepository" }
}
]
}
}
}
EOF
The keyPath here is your local public key — useful only for
images you yourself signed. To verify Red Hat-signed Hummingbird
images, use the Red Hat public key instead. Test thoroughly
before deploying a strict policy: an image that fails the policy
check is unpullable until either the policy is relaxed or the
signature appears.
Verify before moving on
You should now have:
- a Cosign key pair under
~/.config/containers/signing, - a signed image you pushed and signed yourself, and a working
cosign verifyinvocation against it, - an SBOM attestation attached to that image, and
- the ability to inspect the SBOM that ships with a stock Hummingbird image.
Where to go next
CVE scanning takes the same image and scans it with Grype, comparing the result against a non-Hummingbird base image so the “near zero CVE” claim has empirical numbers behind it.