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 for vulnerabilities and blocks deployment on critical CVEs (Gating happens via admission policy; you can also scan pre-push if you need “never publish.”)
- 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
Verify the signed image from the companion repo:
cosign verify ghcr.io/j-dahl7/container-sbom-signing-attestation@sha256:6bd08a4fd7648e0b4f98f2f722f6a62397760aa3926bf9d5bd90a6dcd71ca818 \
--certificate-identity-regexp='^https://github\.com/j-dahl7/container-sbom-signing-attestation/\.github/workflows/supply-chain\.yml@refs/(heads|tags)/.+$' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'Why 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 are driving SBOM adoption (agencies may require them based on criticality)
- 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.
# Multi-stage build
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 go build -o /app main.go
# Distroless runtime
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
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. This is a big deal: production-ready base images with significantly fewer CVEs, SLSA provenance built-in, and automated security rebuilds. If distroless feels too restrictive, DHI gives you a shell and package manager while still dramatically reducing your attack surface.
# 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.
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
In CI/CD
- name: Scan for vulnerabilities
uses: aquasecurity/[email protected]
with:
image-ref: ${{ env.IMAGE }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
# Fail the build on critical vulnerabilities
- name: Block on critical CVEs
uses: aquasecurity/[email protected]
with:
image-ref: ${{ env.IMAGE }}
exit-code: '1'
severity: 'CRITICAL'
ignore-unfixed: true
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"]
}
]
}
Every package, every version, every location. When a CVE drops, grep your SBOMs across all images instantly.
In CI/CD
- name: Generate SBOM
run: |
syft ${{ env.IMAGE }}@${{ steps.build.outputs.digest }} \
--output spdx-json=sbom.spdx.json \
--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.
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@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Note: For best results, add
artifact-metadata: writeto your permissions block.
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/ci\.yml@refs/(heads|tags)/.+$' \
--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/ci\.yml@refs/(heads|tags)/.+$' \
--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/.+@refs/(heads|tags)/.+$"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
Version Note: Use Kyverno 1.14.0-alpha.1 or laterβCVE-2025-29778 affects earlier versions where
subjectRegExp/issuerRegExpcould be ignored or bypassed.
Now unsigned images - or images signed by unauthorized workflows - can’t deploy.
The Complete Workflow
Here’s everything together. Note: I pin actions by SHA (with version comments) for supply-chain hardeningβthe snippets above use tags for readability.
name: Supply Chain Security
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write
attestations: write
artifact-metadata: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Login to GHCR
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
# Build
- uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
id: build
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
# Scan
- uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # 0.28.0
with:
image-ref: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
exit-code: '1'
severity: 'CRITICAL'
# Install supply chain tools
- uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
- uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
# SBOM
- run: syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} -o spdx-json > sbom.json
# Sign
- run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
# Attest SBOM
- run: cosign attest --yes --type spdxjson --predicate sbom.json ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
# Provenance
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
Full working example in the companion repo.
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 - Just add
id-token: writepermission - 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.

