Sub-Module 1.4: Image Signing & Attestation
Introduction
In Sub-Module 1.3, you scanned your image for vulnerabilities and generated an SBOM. But how do you prove to consumers that the image and SBOM they download are authentic and unmodified? This is where cryptographic signing comes in.
In this sub-module, you’ll use cosign to digitally sign your container images and attach signed SBOM attestations, creating a verifiable chain of trust from build to deployment.
Image Signing with Cosign
We’ve looked for known vulnerabilities, detailed the contents, and are ready to publish. How do we ensure that the image that comes out of the registry matches what went in? A common way to ensure a chain of trust is by digitally signing images. One tool to manage container signing is cosign.
Cosign signs and verifies images stored in OCI registries. Since our image lives in local Podman storage, we’ll push the image to Quay for signing.
|
Cosign in Production:
|
Step 1: Log into Quay and re-tag an image
An organization for your lab user has already been created, you can log into Quay with podman using the same credentials as the UI.
podman login {quay_hostname} --username {quay_user} --password {quay_password}
We’ll be using the hostname and username combination a lot for this sub-module, so create an environment variable to clean up some of the later commands. You can also refresh your memory on the Quay URL or username with echo.
export QUAY_ORG="{quay_hostname}/{quay_user}"
podman tag hummingbird-demo:v1 $QUAY_ORG/hummingbird-demo:v1
podman push $QUAY_ORG/hummingbird-demo:v1
Once the image is in the registry, we get the digest for use with cosign.
IMAGE_DIGEST=$(podman inspect --format='{{.Digest}}' $QUAY_ORG/hummingbird-demo:v1)
echo "Image digest: ${IMAGE_DIGEST}"
Image digest: sha256:abc123...
|
We capture the image digest ( |
Step 2: Generate Signing Keys
Generate a key pair for testing (in production, use keyless signing or KMS), for the purposes of this lab we’ll set an empty password via the environment variable.
export COSIGN_PASSWORD=""
cosign generate-key-pair
Private key written to cosign.key Public key written to cosign.pub
|
Production Signing Methods:
For this workshop, we use local keys for simplicity. Never commit |
Step 3: Sign the Image
Sign your image with cosign using the digest captured in Step 1. The --tlog-upload=false flag skips the Rekor transparency log which publishes to the Sigstore public instance. Since these are lab images, we want to skip adding noise to the public good instance of Sigstore.
cosign sign --tlog-upload=false \
--yes --key cosign.key \
${QUAY_ORG}/hummingbird-demo@${IMAGE_DIGEST}
Pushing signature to: {quay_hostname}/{quay_user}/hummingbird-demo
|
The signature is stored as an OCI artifact in the same repository as your image. Cosign appends the signature as a separate manifest with a |
Step 4: Verify the Signature
You can use cosign to create signatures as well as verify the exsisting signature of a container image.
Verify the image signature you just created. In a production setting, your chain of trust would use the official public sources.
cosign verify --insecure-ignore-tlog=true \
--key cosign.pub \
${QUAY_ORG}/hummingbird-demo@${IMAGE_DIGEST}
WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the signature.
Verification for {quay_hostname}/{quay_user}/hummingbird-demo@sha256:abc123... --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"{quay_hostname}/{quay_user}/hummingbird-demo"},...
|
The tlog WARNING is expected. We skipped the Rekor transparency log when signing (Step 3), so we must also skip tlog verification here. In production, both signing and verification should use the transparency log. |
We’ve succesfully signed our image and verified the signature, meaning we can be confident the images are the same on both ends. In a real world example, your key would come from some other source.
Attach SBOM as Attestation
Step 5: Attach SBOM Attestation
We can combine the ideas of signing with the content list of the image to create an 'attestation'. This allows the user to verify that the contents of an image pulled match what you created and pushed to the registry.
|
What is an Attestation? An attestation is metadata about an image (like an SBOM, scan results, or build provenance) that is:
This proves the SBOM came from the same trusted source as the image. |
Attach the SBOM we created with syft in Sub-Module 1.3 as a signed attestation using cosign.
cosign attest --tlog-upload=false \
--yes --key cosign.key \
--predicate ~/scanning/hummingbird-demo.spdx --type spdxjson \
${QUAY_ORG}/hummingbird-demo@${IMAGE_DIGEST}
Using payload from: ~/scanning/hummingbird-demo.spdx
Step 6: Verify SBOM Attestation
Verify the SBOM attestation and check for our package count from the earlier examination of the local SBOM. cosign will print the raw JSON, we can use jq to find and print our value.
cosign verify-attestation --insecure-ignore-tlog=true \
--key cosign.pub \
--type spdxjson \
${QUAY_ORG}/hummingbird-demo@${IMAGE_DIGEST} \
| jq -r '.payload' | base64 -d | jq '.predicate.packages | length'
WARNING: Skipping tlog verification is an insecure practice that lacks of transparency and auditability verification for the signature.
Verification for {quay_hostname}/{quay_user}/hummingbird-demo@sha256:abc123... --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
166
This confirms:
-
The attestation signature is valid
-
The SBOM was signed by the holder of
cosign.key -
The SBOM contains the same package information as expected
You can also download the SBOM for use locally. We’re using attestation instead of attachments for SBOMs to improve verification. Attachments are deprecated in cosign and will eventually be removed.
cosign download attestation --output-file cosign.sbom \
${QUAY_ORG}/hummingbird-demo@${IMAGE_DIGEST}
The SBOM is standard JSON and can be proccessed with any standard tool.
file cosign.sbom
jq -r '.payload' cosign.sbom | base64 -d | jq '.predicate.packages | length'
Summary
Congratulations! You’ve completed Sub-Module 1.4 and established a complete chain of trust for your container images.
✅ Signed container images with cosign
✅ Verified image signatures cryptographically
✅ Attached SBOM as signed attestation
✅ Verified SBOM attestations
✅ Understood trust and provenance workflows
Trust Chain Established:
-
Image Signature: Proves the image hasn’t been tampered with
-
SBOM Attestation: Proves the SBOM belongs to this specific image
-
Verification: Anyone with the public key can verify authenticity
Security Workflow:
-
Build → Multi-stage image with minimal attack surface
-
Scan → Grype checks for CVEs (should be 0)
-
SBOM → Syft generates compliance artifacts
-
Sign → Cosign proves provenance
-
Attest → SBOM attached as signed metadata
-
Verify → Consumers verify signatures before deploying
Production Best Practices:
-
Use keyless signing with OIDC identity (GitHub, GitLab, Google)
-
Store keys in KMS (AWS KMS, Azure Key Vault, GCP KMS, Vault)
-
Enable Rekor transparency log for audit trails
-
Integrate verification into admission controllers (Kyverno, OPA)
Compliance Ready:
-
Signed SBOMs meet NIST and EU Cyber Resilience Act requirements
-
Attestations prove SBOM authenticity and provenance
-
Verification gates prevent unauthorized images from running
Next Steps
In Sub-Module 1.5: Custom Security Configurations, you’ll learn how to customize hardened images for specific security requirements, including custom CA certificates and FIPS compliance.
Troubleshooting
Issue: Cosign sign/verify fails with UNAUTHORIZED error
Cosign requires images to be in an OCI registry — it cannot sign images in local Podman storage. Ensure you are logged into Quay and the image has been pushed:
# Verify you're logged into Quay
podman login {quay_hostname} --get-login
# If not logged in, authenticate
podman login {quay_hostname} --username {quay_user} --password {quay_password}
# Verify the image exists in Quay
podman images | grep hummingbird-demo
# Re-tag and push if needed
podman tag hummingbird-demo:v1 $QUAY_ORG/hummingbird-demo:v1
podman push $QUAY_ORG/hummingbird-demo:v1
Issue: Cosign verification fails
# Ensure you're using the correct public key and digest
cosign verify --key cosign.pub --insecure-ignore-tlog=true ${QUAY_ORG}/hummingbird-demo@${IMAGE_DIGEST}
# If IMAGE_DIGEST is not set, recapture it
IMAGE_DIGEST=$(podman inspect --format='{{.Digest}}' $QUAY_ORG/hummingbird-demo:v1)
echo "Image digest: ${IMAGE_DIGEST}"
Issue: SBOM file not found for attestation
# Verify SBOM was generated in Sub-Module 1.3
ls -la ~/scanning/hummingbird-demo.spdx
# If missing, regenerate it
cd ~/scanning
syft hummingbird-demo:v1 -o spdx-json=hummingbird-demo.spdx