On this page

Over the last couple of weeks, I’ve been diving deep into container supply chain security. Between high-profile incidents like SolarWinds, Log4Shell, and the xz Utils backdoor, it’s clear that securing the build pipeline is just as critical as securing the application itself. I wanted to build out a complete pipeline that handles vulnerability scanning, SBOM generation, image signing, and build provenance - all without managing any long-lived secrets.
Here’s the good news: it’s easier than you might think.
In this post, we’ll build a complete supply chain security pipeline that:
- Scans every pull request and publication candidate before push, then scans the published digest again before signing and attesting it
- Generates a Software Bill of Materials (SBOM) automatically
- Signs every image cryptographically - without managing keys
- Attests build provenance for SLSA compliance
Hands-on Lab: All code is available in the companion repo.
TL;DR:
- SBOMs are increasingly requested (exec orders, audits, procurement)
- Sigstore/Cosign enables keyless signing via OIDC
- GitHub Actions can generate SLSA provenance natively
- The entire pipeline runs with no long-lived secrets
From a companion-repo checkout, verify the exact published image, signed SPDX SBOM, and SLSA provenance:
bash scripts/verify-image.sh \
ghcr.io/j-dahl7/container-sbom-signing-attestation@sha256:d6439134951f052e54465a9bd5f54496481b460d162b252936959d64aad65d86 \
j-dahl7/container-sbom-signing-attestationWhy Supply Chain Security Matters Now
The Wake-Up Calls
SolarWinds (2020): Attackers compromised the build pipeline, injecting malware into signed updates that reached 18,000 organizations.
Log4Shell (2021): A single vulnerable dependency lurking in thousands of applications. Teams scrambled to figure out “do we even use Log4j?”
xz Utils (2024): A trusted maintainer turned out to be a threat actor who spent years gaining trust before backdooring critical compression software.
The New Reality
- US Executive Order 14028 and OMB M-22-18 accelerated federal SBOM adoption historically. OMB M-26-05 rescinded M-22-18 in January 2026 and replaced its government-wide attestation model with agency-specific, risk-based software-security requirements.
- SLSA (Supply chain Levels for Software Artifacts) is becoming the compliance framework of choice
- Auditors are increasingly requesting signed artifacts and provenance documentation
No Long-Lived Secrets
Traditional CI/CD pipelines are filled with long-lived secrets: registry credentials, signing keys, service account tokens. Each one is a potential breach vector.
Our pipeline has no long-lived secrets:
| Component | Traditional | Our Approach |
|---|---|---|
| Registry auth | Stored credentials | GITHUB_TOKEN (automatic) |
| Image signing | Stored private key | OIDC → Sigstore (keyless) |
| Provenance | Manual process | GitHub Attestations (automatic) |
How is this possible? OIDC (OpenID Connect) lets GitHub Actions prove its identity to external services without exchanging secrets. Sigstore issues short-lived signing certificates based on this identity.
Part 1: The Hardened Container
Before we secure the pipeline, let’s secure the image itself.
Why Distroless?
Most container breaches follow the same pattern:
- Exploit application vulnerability
- Drop to shell
- Download tools (
curl,wget) - Escalate privileges
Distroless images have no shell. No package manager. No unnecessary binaries. Just your application and its runtime dependencies.
# Both bases are locked to reviewed immutable manifest digests.
FROM golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder
WORKDIR /build
COPY app/go.mod ./
RUN go mod download
COPY app/*.go ./
RUN go test ./... && CGO_ENABLED=0 go build -trimpath -o /app .
# Distroless runtime
FROM gcr.io/distroless/static-debian12:nonroot@sha256:f5b485ea962d9bd1186b2f6b3a061191539b905b82ec395de78cbfae51f20e35
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
The module’s go directive is a minimum supported toolchain. The pinned builder
must be at least that version and at least the reviewed 1.26.5 security floor,
but a newer builder patch does not need an artificial matching go.mod bump.
Result:
- ~2MB base image (vs ~100MB+ for Ubuntu)
- No shell = reduced post-compromise attack surface
- Non-root by default
- Minimal CVE surface
Docker recently released Docker Hardened Images (DHI) for the community. Runtime variants deliberately omit a shell and package manager; separate -dev variants include development utilities for debugging and build workflows. Choose the variant that matches the stage instead of assuming every DHI is interactive.
# Instead of: FROM python:3.12-slim-bookworm
FROM dhi.io/python:3.12 # requires: docker login dhi.ioCheck Docker's documentation for registry access and availability.
The Security Toolchain
Before we dive into each component, here’s the trio of open-source tools that power our supply chain security pipeline:

Each tool handles a critical piece: Trivy scans for vulnerabilities, Syft generates the software bill of materials, and Cosign handles cryptographic signing. All three integrate seamlessly with GitHub Actions and require zero long-lived secrets. The snippets below explain the concepts; the companion repository pins Actions and base images to immutable commits or digests and is the source of truth for copy-pasteable workflow code.
Part 2: Vulnerability Scanning (Trivy)
Trivy scans container images for:
- OS package vulnerabilities (CVEs)
- Application dependencies (npm, pip, go modules)
- Misconfigurations
- Secrets accidentally baked in
Trivy in CI/CD
- name: Fail on critical image vulnerabilities
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: supply-chain-demo:validation
format: table
severity: CRITICAL
ignore-unfixed: true
exit-code: '1'
- name: Fail on critical published-image vulnerabilities
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: table
severity: CRITICAL
ignore-unfixed: true
exit-code: '1'
Why ignore-unfixed?
Some CVEs have no patch available yet. Blocking on unfixable issues creates alert fatigue without improving security. Focus on what you can actually remediate.
Part 3: SBOM Generation (Syft)
An SBOM (Software Bill of Materials) is an ingredient list for your software. When the next Log4Shell hits, you can instantly answer: “Are we affected?”
Generating SBOMs
# SPDX format (ISO standard)
syft <image> -o spdx-json > sbom.spdx.json
# CycloneDX format (OWASP standard)
syft <image> -o cyclonedx-json > sbom.cdx.json
What’s Inside?
{
"packages": [
{
"name": "golang.org/x/crypto",
"version": "v0.17.0",
"type": "go-module",
"locations": ["/app"]
}
]
}
Detected packages, versions, and locations. When a CVE drops, grep your SBOMs across all images quickly.
SBOM Generation in CI/CD
- name: Generate SBOM
run: |
image_ref="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
syft "$image_ref" --output spdx-json=sbom.spdx.json
syft "$image_ref" --output cyclonedx-json=sbom.cdx.json
Part 4: Keyless Signing (Cosign + Sigstore)
This is the magic. Traditional signing requires:
- Generate a keypair
- Store private key securely (HSM? Vault? Secrets manager?)
- Rotate keys periodically
- Distribute public key to verifiers
Keyless signing with Sigstore requires none of that.
How It Works
- GitHub Actions proves its identity via OIDC token
- Fulcio (Sigstore CA) issues a short-lived certificate
- Cosign signs the artifact with this certificate
- Rekor records the signature in a public transparency log
The certificate encodes WHO signed (GitHub workflow), WHAT repo, and WHEN. Anyone can verify without knowing any keys.
Cosign in CI/CD
permissions:
id-token: write # Required for OIDC
- name: Sign image (keyless)
run: |
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
That’s it. No keys to manage. No secrets to store.
Attesting the SBOM
The signature proves the image is authentic. But we can also attest that a specific SBOM belongs to that image:
- name: Attest SBOM
run: |
cosign attest --yes \
--type spdxjson \
--predicate sbom.spdx.json \
${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
Now the SBOM is cryptographically bound to the image digest.
Part 5: Build Provenance (SLSA)
Provenance answers: “How was this artifact built?”
- What source commit?
- What build system?
- What inputs?
- Who triggered it?
GitHub Native Attestations
- name: Generate provenance
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
This creates a SLSA v1.0 provenance attestation signed using a Sigstore-issued certificate (public repos use public Sigstore; private repos use GitHub’s private Sigstore instance).
SLSA Build Levels
| Level | Requirements |
|---|---|
| Build L1 | Provenance exists, shows how artifact was built |
| Build L2 | Signed provenance, generated by hosted build service |
| Build L3 | Hardened build platform, provenance is non-falsifiable |
GitHub Actions with attestations support SLSA Build L2 out of the box. Achieving full Build L3 requires additional controls—specifically, using reusable workflows to isolate the build and signing logic from the calling repository. The workflow in this post provides strong L2 guarantees with L3 characteristics (ephemeral runners, signed provenance, OIDC-based identity), but strict L3 compliance requires moving the build steps into a separate reusable workflow.
Part 6: Verification (Consumer Side)
All this signing is useless if nobody verifies. Here’s how consumers validate your supply chain:
Verify Signature
cosign verify ghcr.io/org/image@sha256:... \
--certificate-identity-regexp='^https://github\.com/org/repo/\.github/workflows/supply-chain\.yml@refs/(heads/main|tags/v[^/]+)$' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'
What this checks:
- Valid signature exists
- Signed by a GitHub Actions workflow
- From the expected repository
Verify SBOM Attestation
cosign verify-attestation ghcr.io/org/image@sha256:... \
--type spdxjson \
--certificate-identity-regexp='^https://github\.com/org/repo/\.github/workflows/supply-chain\.yml@refs/(heads/main|tags/v[^/]+)$' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'
Extract SBOM
cosign verify-attestation <image@digest> --type spdxjson ... \
| jq -r '.payload' | base64 -d | jq '.predicate'
Kubernetes Policy Enforcement
# Kyverno policy: require keyless signatures from GitHub Actions
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
subjectRegExp: "^https://github\\.com/myorg/myrepo/\\.github/workflows/supply-chain\\.yml@refs/(heads/main|tags/v[^/]+)$"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
Version note: Do not use an alpha build as the production safety floor. Kyverno’s CVE-2025-29778 advisory scopes the vulnerable range to
v1.13.0throughv1.13.5and listsv1.13.6andv1.14.0as patched releases. Use a currently supported stable Kyverno release that includes that fix, and validate this policy against the exact release before enforcement.
Now unsigned images - or images signed by unauthorized workflows - can’t deploy.
The Complete Workflow
The working pipeline separates trust boundaries instead of granting publication permissions to every run:
validatehas onlycontents: read. It runs contract tests and configuration scans, builds the image locally, and blocks critical image vulnerabilities.publishruns only after validation and only formainor av*tag. That job alone receivespackages: write,id-token: write, andattestations: write; it publishes and scans the exact digest before signing it and attaching the SPDX SBOM and provenance.verifyhas read-only package access and requires the signature, SBOM, and provenance to match this repository’s exactsupply-chain.ymlidentity onmainor av*tag. Read-only registry checks retry at most five times with bounded backoff, then fail closed.
All Actions are pinned by full commit SHA in the complete companion workflow.
Zero Trust Principles Applied
| Principle | Implementation |
|---|---|
| Never trust, always verify | Consumers verify signatures before pulling |
| Assume breach | No long-lived secrets to steal - keyless signing |
| Least privilege | Distroless images, non-root users |
| Defense in depth | Scan + Sign + Attest + Provenance |
| Audit everything | Rekor transparency log is immutable |
Next Steps
- Start with scanning - Trivy takes 5 minutes to add
- Add SBOM generation - Another 5 minutes with Syft
- Enable keyless signing - Scope
id-token: writeto the ref-gated publishing job only - Enforce in production - Kyverno/Gatekeeper policies
Resources
- Companion Lab Repo
- Sigstore Documentation
- Cosign Quick Start
- SLSA Specification
- GitHub Attestations
- Trivy Documentation
- Syft Documentation
Have you implemented supply chain security in your pipelines? I’d love to hear about your experience - what worked, what challenges you hit, or questions you’re still working through. Find me on LinkedIn or my other socials linked below.

Jerrad Dahlager, CISSP, CCSP
Cloud Security Architect · Adjunct Instructor
Marine Corps veteran and firm believer that the best security survives contact with reality.
Have thoughts on this post? I'd love to hear from you.
