Linux Supply Chain Security: Practical Guide to Sigstore, SBOMs, and SLSA

Secure your Linux software supply chain with Sigstore Cosign for container signing, Syft for SBOM generation, Grype for vulnerability scanning, and SLSA provenance — with Kyverno policies to enforce trust at deployment.

Software supply chain attacks aren't theoretical anymore — they're front-page news. The XZ Utils backdoor, malicious PyPI packages, compromised npm modules... attackers have figured out that targeting the build pipeline itself is often easier than breaking into production systems directly. And the numbers back it up: supply chain compromises cost organizations an estimated $46 billion annually, with projections to triple by 2031.

The good news? The open-source ecosystem has fought back with three complementary standards that, when combined, form a genuinely solid defense. SBOMs (Software Bill of Materials) give you transparency into what's inside your software. SLSA (Supply-chain Levels for Software Artifacts) ensures build integrity. And Sigstore provides cryptographic signing and verification that's actually practical to use.

This guide walks you through implementing all three on Linux — from signing your first container image to building a fully automated CI/CD pipeline that blocks untrusted artifacts before they ever reach your cluster.

Understanding the Supply Chain Security Trinity

Before we get into the commands, let's take a step back and understand how these three standards fit together. Each one solves a distinct problem, but their real power shows up when you combine them.

SBOM: What's Actually in Your Software?

An SBOM is essentially a machine-readable inventory of every component in a software artifact — libraries, frameworks, transitive dependencies, and their versions. Think of it as a nutritional label for software.

Here's why that matters: when a zero-day like Log4Shell drops, an SBOM lets you answer "are we affected?" in seconds rather than spending days digging through dependency trees manually. I've seen teams scramble for a week during Log4Shell because they had no reliable way to know which services pulled in that library. An SBOM would've turned that into a five-minute query.

Two dominant formats exist:

  • CycloneDX — Developed by OWASP, optimized for security use cases with native support for vulnerability references, VEX (Vulnerability Exploitability eXchange), and services
  • SPDX — A Linux Foundation standard focused on license compliance and package provenance, now at version 3.0 with expanded security metadata

As of 2026, SBOMs aren't optional for many organizations anymore. The EU Cyber Resilience Act (CRA) mandates machine-readable SBOMs for products sold in the European market, and US Executive Order 14028 requires them for software sold to federal agencies. Even if regulations don't apply to you directly, your customers will start asking for them — if they haven't already.

SLSA: How Was Your Software Built?

SLSA (pronounced "salsa") is a framework originally developed by Google, based on their internal Binary Authorization for Borg system. It defines four progressively stricter levels of build integrity:

  • Level 1 — The build process is documented and produces provenance metadata
  • Level 2 — Provenance is generated by a hosted build service (not locally), making it harder for a single actor to forge
  • Level 3 — The build service is hardened, with the build definition coming from an external, version-controlled source
  • Level 4 — Hermetic, reproducible builds with two-party review of all changes

The latest SLSA 1.2 specification (released through the OpenSSF) introduces separate build and source tracks, which is a welcome change — it lets teams incrementally improve each dimension independently rather than having to nail everything at once.

Sigstore: Who Signed Your Software?

Honestly, Sigstore is one of those projects that makes you wonder why it took so long. It makes cryptographic signing about as easy as HTTPS made encryption. Three components work together:

  • Cosign — CLI tool for signing and verifying container images, binaries, and OCI artifacts
  • Fulcio — A free certificate authority that issues short-lived certificates based on OpenID Connect (OIDC) identity
  • Rekor — An immutable transparency log that records every signing event for public audit

The keyless signing flow is the real game-changer. Developers authenticate through their existing identity provider (GitHub, Google, Microsoft), receive a short-lived certificate, sign the artifact, and the private key gets discarded. No long-lived keys to manage, rotate, or — let's be honest — accidentally commit to a public repo.

Setting Up Your Linux Environment

This guide uses the latest stable tool versions as of March 2026. All commands are tested on Ubuntu 24.04 LTS and RHEL 9, but they'll work on any modern Linux distribution.

Installing Cosign v3

Cosign v3 is the current major release, with v3.0.5 being the latest patch. The v3 line introduces standardized bundle formats and OCI Image 1.1 referrers support by default.

# Download and install Cosign v3
curl -sLO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign

# Verify installation
cosign version

Alternatively, if you've got Go 1.22+ installed:

go install github.com/sigstore/cosign/v3/cmd/cosign@latest

Installing Syft for SBOM Generation

# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Verify
syft version

Installing Grype for Vulnerability Scanning

# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# Verify
grype version

Generating SBOMs with Syft

Syft scans container images, filesystems, and archives through ecosystem-specific catalogers — it supports over 28 package ecosystems, from APK and dpkg to Go modules, npm, pip, and Cargo. So whatever your stack looks like, Syft probably has you covered.

Scanning a Container Image

# Generate a CycloneDX JSON SBOM from a container image
syft nginx:1.27 -o cyclonedx-json=nginx-sbom.cdx.json

# Generate an SPDX JSON SBOM
syft nginx:1.27 -o spdx-json=nginx-sbom.spdx.json

# Generate both simultaneously
syft nginx:1.27 \
  -o cyclonedx-json=nginx-sbom.cdx.json \
  -o spdx-json=nginx-sbom.spdx.json

Scanning a Local Project Directory

# Scan the current project directory
syft dir:. -o cyclonedx-json=project-sbom.cdx.json

# Scan a specific path
syft dir:/opt/myapp -o cyclonedx-json=myapp-sbom.cdx.json

Inspecting SBOM Contents

# Count components
jq '.components | length' nginx-sbom.cdx.json

# List all packages with versions
jq -r '.components[] | "\(.name) \(.version)"' nginx-sbom.cdx.json

# Filter for a specific package (useful during zero-day triage)
jq '.components[] | select(.name | test("openssl"))' nginx-sbom.cdx.json

Scanning SBOMs for Vulnerabilities with Grype

Grype aggregates vulnerability data from the NVD, GitHub Security Advisories, and distribution-specific trackers (Debian, Ubuntu, Red Hat, Alpine, Amazon Linux, Oracle). Its database updates daily, and as of 2026, each finding includes an EPSS score — that's the 30-day exploitation probability — alongside the traditional CVSS rating. This makes prioritization way more practical than sorting by CVSS alone.

Scanning an Image Directly

# Scan a container image
grype nginx:1.27

# Filter to Critical and High severity only
grype nginx:1.27 --only-fixed --fail-on high

Scanning a Previously Generated SBOM

# Scan the SBOM (no image pull required)
grype sbom:nginx-sbom.cdx.json

# Output machine-readable JSON for CI/CD integration
grype sbom:nginx-sbom.cdx.json -o json > vulnerabilities.json

# Output SARIF for GitHub Security tab integration
grype sbom:nginx-sbom.cdx.json -o sarif > vulnerabilities.sarif

This SBOM-first workflow is particularly powerful. You generate the SBOM once during your build, then re-scan it whenever new CVEs are disclosed — without needing access to the original image or filesystem. It's faster and it works even if the image has been garbage-collected from your registry.

Setting CI/CD Quality Gates

# Fail the pipeline if any High or Critical vulnerabilities with fixes exist
grype sbom:nginx-sbom.cdx.json --only-fixed --fail-on high
echo $?  # Non-zero exit code means policy violation

Signing Container Images with Cosign

Image signing creates a cryptographic proof that a specific image digest was approved by a known identity. Cosign supports two approaches: key-based signing for air-gapped or highly regulated environments, and keyless signing for standard CI/CD workflows.

Key-Based Signing

# Generate a key pair (you will be prompted for a password)
cosign generate-key-pair

# This creates:
#   cosign.key  - Private key (encrypted with your password)
#   cosign.pub  - Public key (distribute for verification)

# Build, push, and sign an image
docker build -t ghcr.io/myorg/myapp:v1.2.0 .
docker push ghcr.io/myorg/myapp:v1.2.0

# Sign the image (prompts for key password)
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.2.0

Verifying a Signed Image

# Verify with the public key
cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.2.0

# Verify and display certificate details
cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.2.0 | jq .

Keyless Signing with OIDC

Keyless signing is the recommended approach for most teams — and in my experience, it's the one you should default to unless you have a specific regulatory requirement for key-based signing. Cosign uses your OIDC identity (GitHub, Google, or Microsoft) to obtain a short-lived certificate from Fulcio, signs the artifact, records the event in Rekor, and discards the private key.

# Keyless sign (opens a browser for OIDC authentication)
cosign sign ghcr.io/myorg/myapp:v1.2.0

# In CI/CD, the OIDC token is provided automatically
# (no browser interaction needed — uses workload identity)
COSIGN_EXPERIMENTAL=1 cosign sign ghcr.io/myorg/myapp:v1.2.0
# Keyless verification requires specifying the expected identity
cosign verify ghcr.io/myorg/myapp:v1.2.0 \
  --certificate-identity=https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com

Adding Annotations to Signatures

# Add metadata to the signature
cosign sign --key cosign.key \
  -a env=production \
  -a commit=$(git rev-parse HEAD) \
  -a pipeline=github-actions \
  ghcr.io/myorg/myapp:v1.2.0

Attaching SBOMs to Container Images

Cosign can attach SBOMs directly to container images as OCI artifacts. This keeps the SBOM co-located with the image it describes — wherever the image travels, its ingredient list follows. No separate artifact repository, no out-of-band distribution.

# Generate the SBOM
syft ghcr.io/myorg/myapp:v1.2.0 -o cyclonedx-json=sbom.cdx.json

# Attach the SBOM to the image
cosign attach sbom --sbom sbom.cdx.json ghcr.io/myorg/myapp:v1.2.0

# Sign the attached SBOM for tamper-evidence
cosign sign --key cosign.key --attachment sbom ghcr.io/myorg/myapp:v1.2.0

# Verify the SBOM attachment
cosign verify --key cosign.pub --attachment sbom ghcr.io/myorg/myapp:v1.2.0

Alternatively, you can use cosign attest to create an in-toto attestation that wraps the SBOM in a signed envelope — this is the preferred approach for SLSA compliance:

# Create a signed SBOM attestation
cosign attest --key cosign.key \
  --type cyclonedx \
  --predicate sbom.cdx.json \
  ghcr.io/myorg/myapp:v1.2.0

# Verify the attestation
cosign verify-attestation --key cosign.pub \
  --type cyclonedx \
  ghcr.io/myorg/myapp:v1.2.0 | jq -r .payload | base64 -d | jq .

Generating SLSA Provenance in CI/CD

SLSA provenance is metadata that records exactly how an artifact was built: the source repository, build instructions, builder identity, and input materials. If someone asks "prove this binary came from that source code, built by that pipeline," provenance is your answer.

GitHub Actions provides built-in support for generating SLSA Level 2-3 provenance through artifact attestations.

GitHub Actions: Built-in Attestations (SLSA Level 2)

GitHub's native actions/attest-build-provenance action generates signed provenance using Sigstore infrastructure. For public repos, it uses the public Sigstore instance; private repositories get GitHub's private Sigstore instance.

Here's a complete workflow that builds an image, generates an SBOM, signs everything, and produces SLSA provenance:

# .github/workflows/build-and-attest.yml
name: Build and Attest

on:
  push:
    tags: ['v*']

permissions:
  contents: read
  packages: write
  id-token: write
  attestations: write

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}

      - name: Install Syft
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
            | sh -s -- -b /usr/local/bin

      - name: Generate SBOM
        run: |
          syft ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} \
            -o cyclonedx-json=sbom.cdx.json

      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign image (keyless)
        run: |
          cosign sign --yes \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}

      - name: Attest SBOM
        run: |
          cosign attest --yes \
            --type cyclonedx \
            --predicate sbom.cdx.json \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}

      - name: Generate SLSA provenance
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

SLSA Level 3 with Reusable Workflows

For SLSA Level 3, the build definition has to come from an external source — a reusable workflow that the developer can't modify in their own repository. This is what separates Level 2 from Level 3: you're removing the developer from the trust boundary of the build process itself.

The slsa-framework/slsa-github-generator project provides pre-built reusable workflows for containers, Go binaries, and generic artifacts.

# .github/workflows/release.yml (caller workflow)
name: Release

on:
  push:
    tags: ['v*']

permissions: read-all

jobs:
  build:
    permissions:
      id-token: write
      contents: write
      actions: read
      packages: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
    with:
      image: ghcr.io/${{ github.repository }}
      digest: ${{ needs.build-image.outputs.digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

Verifying SLSA Provenance

# Using the GitHub CLI
gh attestation verify oci://ghcr.io/myorg/myapp:v1.2.0 --repo myorg/myapp

# Using slsa-verifier
slsa-verifier verify-image ghcr.io/myorg/myapp:v1.2.0 \
  --source-uri github.com/myorg/myapp \
  --source-tag v1.2.0

Enforcing Signatures at Deploy Time with Kyverno

Here's the thing about signing images — it's only half the story. Signing means nothing if unsigned images can still be deployed. You need enforcement at the gate.

Kyverno is a Kubernetes-native policy engine that can verify Cosign signatures and attestations at admission time, blocking any pod that references an unsigned or untrusted image. It's the bouncer at the door.

Installing Kyverno

# Install Kyverno via Helm
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update

helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace \
  --set replicaCount=3

Policy: Require Image Signatures (Key-Based)

# require-signed-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----
          mutateDigest: true
          verifyDigest: true

Policy: Require Keyless Signatures from GitHub Actions

# require-keyless-signatures.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-github-actions-signatures
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-keyless-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - count: 1
              entries:
                - keyless:
                    subject: "https://github.com/myorg/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev

Policy: Require SBOM Attestation

# require-sbom-attestation.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-sbom-attestation
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-sbom-exists
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestations:
            - type: https://cyclonedx.org/bom
              attestors:
                - entries:
                    - keyless:
                        subject: "https://github.com/myorg/*"
                        issuer: "https://token.actions.githubusercontent.com"

Testing Policy Enforcement

# Apply the policy
kubectl apply -f require-signed-images.yaml

# Test with an unsigned image (should be rejected)
kubectl run test --image=ghcr.io/myorg/myapp:unsigned
# Error: admission webhook denied the request:
# image verification failed for ghcr.io/myorg/myapp:unsigned

# Test with a signed image (should succeed)
kubectl run test --image=ghcr.io/myorg/myapp:v1.2.0

Building the Complete Pipeline

So, let's bring everything together. Here's the end-to-end flow that achieves SLSA Level 2+ with SBOM and signature enforcement:

  1. Build — Compile or package the application in a hosted CI environment (GitHub Actions runner)
  2. Generate SBOM — Syft produces a CycloneDX SBOM of the built container image
  3. Scan for vulnerabilities — Grype scans the SBOM and fails the pipeline if Critical/High fixed CVEs are found
  4. Push image — The container image is pushed to the OCI registry
  5. Sign image — Cosign keyless-signs the image digest using the GitHub OIDC token
  6. Attach and sign SBOM — The SBOM is attached as a signed attestation via cosign attest
  7. Generate provenance — GitHub attestation action creates SLSA provenance metadata
  8. Deploy — Kyverno verifies the signature, SBOM attestation, and optionally provenance before admitting the pod

Here's what that looks like as a Makefile target for local testing:

# Complete pipeline summary as a Makefile target
.PHONY: secure-release
secure-release:
	# Step 1: Build
	docker build -t $(REGISTRY)/$(IMAGE):$(TAG) .
	docker push $(REGISTRY)/$(IMAGE):$(TAG)

	# Step 2: Generate SBOM
	syft $(REGISTRY)/$(IMAGE):$(TAG) -o cyclonedx-json=sbom.cdx.json

	# Step 3: Vulnerability gate
	grype sbom:sbom.cdx.json --only-fixed --fail-on high

	# Step 4: Sign the image
	cosign sign --yes $(REGISTRY)/$(IMAGE)@$(DIGEST)

	# Step 5: Attach signed SBOM attestation
	cosign attest --yes --type cyclonedx \
	  --predicate sbom.cdx.json \
	  $(REGISTRY)/$(IMAGE)@$(DIGEST)

	@echo "Release $(TAG) signed, attested, and ready for deploy"

Operational Best Practices

Key Management for Regulated Environments

While keyless signing works great for most teams, regulated industries (finance, healthcare, defense) may need key-based signing with hardware security modules. Cosign supports KMS providers natively:

# Sign using AWS KMS
cosign sign --key awskms:///arn:aws:kms:us-east-1:123456:key/abcdef \
  ghcr.io/myorg/myapp:v1.2.0

# Sign using Google Cloud KMS
cosign sign --key gcpkms://projects/myproject/locations/global/keyRings/myring/cryptoKeys/mykey \
  ghcr.io/myorg/myapp:v1.2.0

# Sign using HashiCorp Vault
cosign sign --key hashivault://transit/keys/cosign \
  ghcr.io/myorg/myapp:v1.2.0

Continuous SBOM Re-scanning

Vulnerabilities don't stop being disclosed after your build finishes. New CVEs show up daily, and that image you scanned clean last month might have three new Critical findings today. Set up a daily cron job or scheduled pipeline to re-scan stored SBOMs against the latest vulnerability database:

#!/bin/bash
# cron-rescan.sh — Run daily via cron or systemd timer
set -euo pipefail

SBOM_DIR="/var/lib/sboms"
ALERT_THRESHOLD="high"

grype db update

for sbom in "${SBOM_DIR}"/*.cdx.json; do
  image_name=$(basename "$sbom" .cdx.json)
  grype "sbom:${sbom}" --only-fixed \
    --fail-on "${ALERT_THRESHOLD}" -o json || {
    echo "[ALERT] New vulnerabilities found in ${image_name}"
    # Send alert via your preferred channel (webhook, email, PagerDuty)
  }
done

Monitoring and Auditing

Rekor provides a public, tamper-proof transparency log for all signing events. You can query it to audit who signed what and when — useful for compliance reviews and incident investigations:

# Search Rekor for all signatures by your CI identity
rekor-cli search --email [email protected]

# Retrieve a specific log entry
rekor-cli get --uuid 24296fb24b8ad77a...

# Verify an entry is in the log
rekor-cli verify --artifact myapp \
  --signature myapp.sig \
  --pki-format x509 \
  --public-key cosign.pub

Frequently Asked Questions

What's the difference between an SBOM and SLSA provenance?

An SBOM answers "what ingredients are in this software?" — it lists every component, library, and dependency. SLSA provenance answers "how was this software built?" — it records the source repository, build system, build instructions, and builder identity. Both are metadata about an artifact but they serve different purposes: SBOM for vulnerability management and license compliance, SLSA for build integrity and tamper detection. Most mature pipelines generate both, and you really should too.

Is keyless signing with Sigstore secure enough for production?

Yes — and in many ways it's more secure than traditional key-based signing. Keyless signing eliminates the most common risk in code signing: long-lived private key compromise. The private key exists only in ephemeral memory during the signing operation and is never written to disk. The signing event gets recorded in Rekor's immutable transparency log, providing a public audit trail. Major projects including Kubernetes, Distroless, and Chainguard images all use keyless signing in production. For environments that require offline verification or regulatory key custody, Cosign supports KMS-backed keys as an alternative.

How do I handle SBOM generation for multi-stage Docker builds?

Run Syft against the final image, not the build stages. Multi-stage builds typically produce a minimal runtime image, and Syft will catalog only what's present in the final layer. If you need to capture build-time dependencies for compliance, generate a separate SBOM from the builder stage: syft docker:myapp-builder -o cyclonedx-json=build-sbom.cdx.json and attach both SBOMs as attestations.

Can I enforce supply chain policies without Kubernetes?

Absolutely. While Kyverno provides Kubernetes-native admission control, you can enforce policies in other environments too. For standalone Linux servers, verify signatures in your deployment scripts using cosign verify before pulling or running images. For Docker Compose environments, add a verification step in your deployment pipeline. For systemd-based deployments, you can create a pre-start ExecStartPre directive that calls cosign verify. The signing and verification tools work independently of any orchestration platform.

How much does Sigstore cost to use?

It's completely free. The public Sigstore infrastructure — Fulcio, Rekor, and the OIDC integration — is open source and backed by the OpenSSF and Linux Foundation. There are no API keys, accounts, or usage limits for the public instance. Organizations that need private transparency logs or air-gapped signing can self-host the entire Sigstore stack using the Sigstore Helm charts, also at no cost beyond your own infrastructure.

About the Author Editorial Team

Our team of expert writers and editors.