On this page

Container Supply Chain Security Pipeline

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
πŸ”¬ Try It Yourself

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:

ComponentTraditionalOur Approach
Registry authStored credentialsGITHUB_TOKEN (automatic)
Image signingStored private keyOIDC β†’ Sigstore (keyless)
ProvenanceManual processGitHub 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:

  1. Exploit application vulnerability
  2. Drop to shell
  3. Download tools (curl, wget)
  4. 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 Hardened Images New

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.io

Check 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:

Supply Chain Security Tools - Cosign, Syft, and Trivy

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
OIDC Token
"I am workflow in repo X"
β†’
πŸ“œ
Fulcio
Certificate CA
"Here's a cert for 10 min"
β†’
πŸ“’
Rekor
Transparency Log
"Signature recorded forever"
Identity
β†’
Certificate
β†’
Immutable Record
  1. GitHub Actions proves its identity via OIDC token
  2. Fulcio (Sigstore CA) issues a short-lived certificate
  3. Cosign signs the artifact with this certificate
  4. 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: write to 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

LevelRequirements
Build L1Provenance exists, shows how artifact was built
Build L2Signed provenance, generated by hosted build service
Build L3Hardened 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/issuerRegExp could 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

PrincipleImplementation
Never trust, always verifyConsumers verify signatures before pulling
Assume breachNo long-lived secrets to steal - keyless signing
Least privilegeDistroless images, non-root users
Defense in depthScan + Sign + Attest + Provenance
Audit everythingRekor transparency log is immutable

Next Steps

  1. Start with scanning - Trivy takes 5 minutes to add
  2. Add SBOM generation - Another 5 minutes with Syft
  3. Enable keyless signing - Just add id-token: write permission
  4. Enforce in production - Kyverno/Gatekeeper policies

Resources



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

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.