Docker Advanced #4: SBOM and Signing — The Entry to Supply Chain Security
#3’s security checklist looked at a single container. This post takes one step up — to the supply chain. What’s in this image, and who built it.
This post in the Docker Advanced series:
- #1 BuildKit and buildx
- #2 Multi-architecture images
- #3 Image security — non-root, distroless, scan (Trivy)
- #4 SBOM and signing (cosign) ← this post
- #5 Resource limits and cgroups
- #6 Production operations — restart policy, healthcheck, graceful shutdown
Why supply chain security suddenly matters #
Several large incidents have hit in recent years.
- 2020 SolarWinds — a properly signed build artifact contained a backdoor
- 2021 codecov bash uploader — a download script for a legit tool was tampered with, leaking secrets
- 2024 xz utils backdoor — a trusted maintainer of a widely used compression tool inserted a backdoor
The common thread — the vulnerability wasn’t in the code, but in the process that built and shipped it. CVE scans alone don’t catch this. The two pillars are SBOM and signing.
| Question | Tool |
|---|---|
| What exactly is inside this image? | SBOM (Software Bill of Materials) |
| Was this image really built by that person/org? | Signing (cosign / sigstore) |
| Through what process was this image built? | attestation / SLSA |
What is an SBOM #
An SBOM is a machine-readable inventory of what a piece of software is composed of. Think of it as a nutrition label for software.
Two standard formats:
| Format | Maintained by | Notes |
|---|---|---|
| SPDX | Linux Foundation | Oldest standard, strong on license info |
| CycloneDX | OWASP | Security-first, rich dependency-graph representation |
Most tools can output either. Pick one and stay consistent for your first use.
Generating an SBOM with Syft #
syft (Anchore) is close to a standard.
brew install syft
# or
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/binsyft myapp:1.0 -o cyclonedx-json > sbom.cdx.json
syft myapp:1.0 -o spdx-json > sbom.spdx.jsonSample output (excerpt of CycloneDX):
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{
"name": "openssl",
"version": "3.0.13-1ubuntu0.1",
"type": "library",
"purl": "pkg:deb/ubuntu/openssl@3.0.13-1ubuntu0.1?arch=amd64"
},
{
"name": "flask",
"version": "3.0.3",
"type": "library",
"purl": "pkg:pypi/flask@3.0.3"
}
]
}The purl (Package URL) for each component is the precise identifier. pkg:pypi/flask@3.0.3 is a standard URL pointing at Flask 3.0.3 on PyPI.
Built-in via docker buildx
#
You can attach the SBOM as an attestation at build time.
docker buildx build \
--sbom=true \
--provenance=mode=max \
-t ghcr.io/me/myapp:1.0 \
--push .--sbom=true— generate an SBOM and attach it as an attestation--provenance=mode=max— include build provenance (which command built what, from what input)
The pushed image carries the attestation manifest alongside the regular ones in the manifest list. Inspect with imagetools:
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
--format '{{json .SBOM}}'This is the cleanest path — generate the SBOM during the build, store it in the same manifest.
What you actually use SBOMs for #
The SBOM itself is just data. Value comes from the tools you point at it.
Vulnerability scan — Grype #
grype sbom:./sbom.cdx.jsonNo image download — scan from the SBOM file alone. Fast for CI caches and post-hoc audits in production.
License compliance #
syft myapp:1.0 -o spdx-tag-value | grep PackageLicenseDeclared | sort -uWhat OSS licenses are inside the image — common in compliance reporting.
Incident response #
When a new CVE drops (e.g., log4shell, xz), use the SBOM to query which already-running images contain that library. Reporting your blast radius within the first hour after a CVE drops is hard without an SBOM.
Signing — Cosign #
Cosign verifies that an image was actually built by a given person / organization. Cosign from the Sigstore project has become the standard.
brew install cosignKey-based signing (older approach) #
cosign generate-key-pair
# cosign.key, cosign.pubcosign sign --key cosign.key ghcr.io/me/myapp:1.0Signature is attached to the image and stored in the registry.
cosign verify --key cosign.pub ghcr.io/me/myapp:1.0The downside is key management. A leaked key destroys trust. The next section’s keyless signing solves that.
Keyless signing — OIDC + Fulcio + Rekor #
The big idea in Sigstore: sign without a long-term key, using OIDC identity, then record the signing event in a public transparency log (Rekor).
cosign sign ghcr.io/me/myapp:1.0
# Browser opens for OIDC sign-in (or automated in CI)
# Fulcio issues a short-lived certificate
# Used to sign
# Recorded in Rekor (public transparency log)cosign verify \
--certificate-identity-regexp 'https://github.com/me/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/me/myapp:1.0certificate-identity-regexp— who is allowed to sign (URL pattern)certificate-oidc-issuer— which OIDC issuer’s certificates to trust
Builds running on GitHub Actions get an OIDC token automatically, so signing happens without env-var or secret management. In production, keyless is almost always the safer choice.
Putting it together in GitHub Actions #
name: build and sign
on: { push: { branches: [main] } }
permissions:
contents: read
packages: write
id-token: write # required for keyless signing
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
sbom: true # SBOM attestation
provenance: mode=max # provenance attestation
- uses: sigstore/cosign-installer@v3
- name: Sign image
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes ghcr.io/${{ github.repository }}@${DIGEST}In one workflow:
- Build the image, multi-arch (#2)
- Attach SBOM + provenance attestations
- Keyless sign (and record in Rekor)
The whole supply chain flow can live in a single workflow.
Attestation — bundling SBOM and signature #
A picture of all the pieces:
image
│
├── manifest (linux/amd64)
├── manifest (linux/arm64)
├── attestation: SBOM (CycloneDX)
├── attestation: provenance (SLSA build info)
└── signature (cosign)All of it lives in the same OCI registry as metadata for the same image. imagetools inspect peers in.
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
--format '{{json .}}' | jq '.Manifest'Verification policies vary, but a common production pattern:
- Does this image have an SBOM attached? — reject otherwise
- Was it signed by our org’s OIDC identity? — reject otherwise
- Does the SBOM contain critical CVEs? — reject if so
Admission control — enforced at the cluster #
In environments like Kubernetes, an admission controller enforces these policies.
| Tool | Role |
|---|---|
| Kyverno | YAML-based image verification — verifyImages |
| OPA Gatekeeper | Policies in Rego |
| Sigstore Policy Controller | dedicated cosign verification |
You don’t usually need this with Docker on a single host, but it’s natural once you operate on K8s.
SLSA — supply chain maturity model #
SLSA (Supply-chain Levels for Software Artifacts) is a framework that defines levels of supply chain security.
| Level | What’s required |
|---|---|
| L1 | Build process is documented |
| L2 | Build runs on a hosted service — provenance generated automatically |
| L3 | Build runs in an isolated environment, provenance is tamper-resistant |
| L4 (legacy) | Reproducible builds, two-party verification |
A GitHub Actions hosted runner + --provenance=mode=max + cosign keyless signing already satisfies most of L3. You can climb the levels gradually without going all-in.
Common pitfalls / mistakes #
- Generating SBOMs in production but not verifying — the SBOMs are useless. Make verification a CI / admission gate.
- Lost or leaked keys (key-based signing) — migrate to keyless.
- Multiple builds overwriting the same tag — tags like
latestbreak attestation traceability. Verify by digest (@sha256:...). - Building when Rekor is down — keyless signing fails. Wrap signing in retry logic.
- SBOM components don’t trace to a real source — some static binaries elude syft. Build-time SBOM (
buildx --sbom) is more accurate.
Wrap-up #
The picture from this post:
- Supply chain threats come from the build process, not the code itself — CVE scans alone aren’t enough.
- An SBOM is a machine-readable manifest of what’s in the image (CycloneDX / SPDX).
- Generate SBOMs with Syft; attach attestations at build time with
buildx --sbom=true. - Cosign keyless signing = OIDC identity + short-lived certs (Fulcio) + transparency log (Rekor) — no long-term key management.
- One GHA workflow covers build → multi-arch → SBOM → provenance → keyless sign.
- In production, make verification a gate — attaching signatures and SBOMs without enforcement is theater.
- SLSA levels are incremental — climb them one step at a time.
In the next post (#5 Resource limits and cgroups) we shift to resource limits. cgroups v2 basics, the behavior of mem_limit / cpus, debugging OOMKilled, and other isolation knobs like ulimit / pids.