LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Supply Chain Security: SBOMs, Sigstore, Cosign, and SLSA

securitysupply-chainsbomsigstorecosignslsacontainersdevopsci-cd

The SolarWinds breach. The Log4Shell vulnerability. The xz-utils backdoor. What these incidents have in common is that the attack didn’t come through the front door — it came through the software supply chain. Trusted code, from trusted sources, delivered through trusted update mechanisms, was weaponised against the people trusting it.

Software supply chain security is the discipline of knowing exactly what’s in your software, verifying it was built the way it claims, and detecting when something unexpected slips in. This guide covers the four pillars of modern supply chain security: SBOMs (knowing what you have), Sigstore/Cosign (proving who built it and that it hasn’t been tampered with), SLSA (attesting how it was built), and integrating all of this into CI/CD.


The Problem: You Don’t Know What You’re Running

A typical containerised application pulls in:

  • A base OS image (Ubuntu, Alpine, Debian)
  • Runtime dependencies (Node modules, Python packages, Go modules)
  • Build tools that may leave artifacts behind
  • Transitive dependencies of dependencies — often hundreds deep

Before modern supply chain tooling, you had limited visibility into any of this. You knew your direct dependencies. You didn’t know what they depended on, which of those had CVEs, or whether the container image you pulled from Docker Hub was actually the one the maintainer built.

Three questions every team should be able to answer:

  1. What’s in it? — SBOM
  2. Did the right person build it? — Artifact signing (Cosign/Sigstore)
  3. Was it built the right way? — SLSA provenance

SBOMs: A Bill of Materials for Software

A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in a software artifact — libraries, packages, licenses, versions, and their relationships. Think of it as the ingredient list on food packaging, but for software.

Two dominant formats:

  • SPDX (Software Package Data Exchange) — ISO/IEC 5962:2021 standard, backed by the Linux Foundation
  • CycloneDX — OWASP standard, focused on security use cases, wider tool support

Generating SBOMs with Syft

Syft by Anchore is the most capable open-source SBOM generator. It understands container images, filesystems, and source trees.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Generate an SBOM from a container image
syft nginx:latest -o spdx-json > nginx-sbom.spdx.json
syft nginx:latest -o cyclonedx-json > nginx-sbom.cdx.json

# Human-readable table output
syft nginx:latest
# NAME                 VERSION          TYPE
# adduser              3.134            deb
# apt                  2.6.1            deb
# base-files           12.4+deb12u5     deb
# bash                 5.2.15-2+b8      deb
# ...
# libssl3              3.0.13-1~deb12u1 deb   ← potential CVE target
# nginx                1.25.4-2~bookworm deb
# openssl              3.0.13-1~deb12u1 deb

# SBOM from a local directory (source code)
syft dir:./myapp -o cyclonedx-json > myapp-source-sbom.cdx.json

# SBOM from a local Docker image tarball
docker save myapp:latest | syft -o spdx-json -

# SBOM from a running container's filesystem
syft container:my-running-container -o cyclonedx-json > running-sbom.cdx.json

The SBOM output is a JSON file listing every package. Here’s an excerpt of CycloneDX format:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "metadata": {
    "timestamp": "2026-03-26T10:00:00Z",
    "component": {
      "type": "container",
      "name": "nginx",
      "version": "latest"
    }
  },
  "components": [
    {
      "type": "library",
      "name": "openssl",
      "version": "3.0.13-1~deb12u1",
      "purl": "pkg:deb/debian/openssl@3.0.13-1~deb12u1",
      "licenses": [{ "license": { "id": "Apache-2.0" } }]
    }
  ]
}

Scanning SBOMs for Vulnerabilities with Grype

Grype scans SBOMs (and images directly) against vulnerability databases:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# Scan a container image
grype nginx:latest

# Scan an SBOM file (much faster — no image pull needed)
grype sbom:nginx-sbom.spdx.json

# Example output:
# NAME        INSTALLED              FIXED-IN   TYPE  VULNERABILITY   SEVERITY
# libssl3     3.0.13-1~deb12u1       3.0.14     deb   CVE-2024-0727   Medium
# openssl     3.0.13-1~deb12u1       3.0.14     deb   CVE-2024-0727   Medium
# ...

# Output only critical/high findings
grype nginx:latest --fail-on high

# JSON output for CI integration
grype nginx:latest -o json > grype-results.json

# Scan only fixed vulnerabilities (ones that have a patch available)
grype nginx:latest --only-fixed

SBOM in CI/CD

Generate and store an SBOM as part of every build:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: myapp:${{ github.sha }}
          format: spdx-json
          output-file: sbom.spdx.json
          artifact-name: sbom.spdx.json

      - name: Scan SBOM for vulnerabilities
        uses: anchore/scan-action@v3
        with:
          sbom: sbom.spdx.json
          fail-build: true
          severity-cutoff: critical

Cosign and Sigstore: Signing Everything

Knowing what’s in your software doesn’t help if you can’t trust where it came from. Artifact signing lets you cryptographically verify that an image or binary was produced by a specific identity and hasn’t been modified since.

Sigstore is a Linux Foundation project that makes signing and verification as easy as running a single command. Its key innovation is keyless signing via OIDC: instead of managing private keys, you sign using a short-lived certificate tied to your identity (GitHub Actions, Google, GitHub account). The certificate is recorded in a public, append-only transparency log (Rekor) — anyone can verify the signature.

Cosign is the CLI tool for signing container images and other artifacts.

Installing Cosign

1
2
3
4
5
6
7
# Linux
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign

# Verify the install
cosign version

Keyless Signing in GitHub Actions

The most important pattern: sign automatically during CI using the OIDC identity of the GitHub Actions workflow itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# .github/workflows/build-and-sign.yml
name: Build, Push, and Sign

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write    # REQUIRED for keyless signing
  packages: write

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

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

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

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          # Capture the digest for signing
          outputs: type=image,push=true

      - name: Sign the container image (keyless)
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          cosign sign --yes \
            ghcr.io/${{ github.repository }}@${DIGEST}
          # This:
          # 1. Gets a short-lived OIDC token from GitHub Actions
          # 2. Exchanges it for a certificate from Fulcio (Sigstore's CA)
          # 3. Signs the image digest with that certificate
          # 4. Records the signature in Rekor (public transparency log)

      - name: Attach SBOM as attestation
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          # Generate SBOM
          syft ghcr.io/${{ github.repository }}@${DIGEST} \
            -o spdx-json > sbom.spdx.json

          # Attach SBOM as a signed attestation on the image
          cosign attest --yes \
            --predicate sbom.spdx.json \
            --type spdxjson \
            ghcr.io/${{ github.repository }}@${DIGEST}

Verifying Signatures

Anyone pulling your image can verify it was signed by your GitHub Actions workflow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Verify the signature
cosign verify \
  --certificate-identity-regexp "https://github.com/myorg/myrepo/.github/workflows/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myrepo:sha256-abc123...

# Output if valid:
# Verification for ghcr.io/myorg/myrepo:sha256-abc123... --
# The following checks were performed on each of these signatures:
#   - The cosign claims were validated
#   - Existence of the claims in the transparency log was verified offline
#   - The code-signing certificate claims were verified
# [{"critical":{"identity":{"docker-reference":"ghcr.io/myorg/myrepo"},...}]

# Verify and extract the SBOM attestation
cosign verify-attestation \
  --certificate-identity-regexp "https://github.com/myorg/myrepo/.github/workflows/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --type spdxjson \
  ghcr.io/myorg/myrepo:sha256-abc123... \
  | jq -r '.payload' | base64 -d | jq .

Key-Based Signing (for Air-Gapped or Private Environments)

For environments without OIDC, use traditional key pairs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Generate a signing key pair
cosign generate-key-pair
# Outputs cosign.key (private) and cosign.pub (public)
# Protect cosign.key — treat it as a high-value secret (store in Vault or HSM)

# Sign an image
cosign sign --key cosign.key ghcr.io/myorg/myrepo:latest

# Verify with public key
cosign verify --key cosign.pub ghcr.io/myorg/myrepo:latest

# Sign a binary file
cosign sign-blob --key cosign.key \
  --output-signature myapp.sig \
  --output-certificate myapp.pem \
  myapp-linux-amd64

# Verify a blob
cosign verify-blob \
  --key cosign.pub \
  --signature myapp.sig \
  myapp-linux-amd64

Signing Other Artifacts (Helm Charts, Files)

Cosign works beyond container images:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Sign a Helm chart
helm package ./mychart
cosign sign-blob --yes mychart-1.0.0.tgz \
  --output-signature mychart-1.0.0.tgz.sig \
  --output-certificate mychart-1.0.0.tgz.pem

# Sign an SBOM file itself
cosign sign-blob --yes sbom.spdx.json \
  --output-signature sbom.spdx.json.sig \
  --output-certificate sbom.spdx.json.pem

# Sign an OCI artifact (non-image)
cosign upload blob -f myfile.tar.gz \
  ghcr.io/myorg/artifacts:myfile
cosign sign --yes ghcr.io/myorg/artifacts:myfile

SLSA: Attesting How Software Was Built

SLSA (Supply-chain Levels for Software Artifacts, pronounced “salsa”) is a security framework from Google, now under the OpenSSF umbrella. Where Cosign tells you who signed the artifact, SLSA tells you how it was built — specifically, whether the build process was trustworthy.

SLSA defines four levels of assurance:

Level Requirements What It Prevents
SLSA 1 Provenance exists Accidental mistakes, basic tampering
SLSA 2 Signed provenance from a hosted build service Tampering after the fact
SLSA 3 Hardened build platform, non-forgeable provenance Insider threats on the build system
SLSA 4 Two-person review, hermetic builds Sophisticated insider attacks

Most teams should target SLSA 2 initially, with SLSA 3 as a goal for critical software.

What Is Provenance?

Provenance is a signed document that records:

  • Which source repository the build came from
  • Which exact commit was built (git SHA)
  • Which build system produced it
  • What inputs (dependencies) were used
  • The output artifact’s digest

A SLSA Level 2 provenance document looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://slsa.dev/provenance/v0.2",
  "subject": [{
    "name": "ghcr.io/myorg/myrepo",
    "digest": { "sha256": "abc123..." }
  }],
  "predicate": {
    "builder": {
      "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v1.9.0"
    },
    "buildType": "https://github.com/slsa-framework/slsa-github-generator/container@v1",
    "invocation": {
      "configSource": {
        "uri": "git+https://github.com/myorg/myrepo@refs/heads/main",
        "digest": { "sha1": "deadbeef..." },
        "entryPoint": ".github/workflows/build.yml"
      }
    },
    "metadata": {
      "buildStartedOn": "2026-03-26T10:00:00Z",
      "completeness": {
        "parameters": true,
        "environment": false,
        "materials": true
      }
    }
  }
}

Generating SLSA Provenance with the GitHub Generator

The SLSA GitHub Generator is the easiest way to reach SLSA Level 3 for GitHub Actions builds. It runs in a separate, isolated workflow that GitHub itself signs — making the provenance non-forgeable even by your own CI admins.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# .github/workflows/build.yml
name: Build and Publish

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

permissions:
  contents: read

jobs:
  build:
    outputs:
      image: ${{ steps.image.outputs.image }}
      digest: ${{ steps.build.outputs.digest }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Set image name
        id: image
        run: echo "image=ghcr.io/${{ github.repository }}" >> "$GITHUB_OUTPUT"

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ steps.image.outputs.image }}:${{ github.ref_name }}

  # This job runs in a separate GitHub-controlled environment
  # Its provenance is signed by GitHub's own key — SLSA Level 3
  provenance:
    needs: build
    permissions:
      actions: read
      id-token: write
      packages: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
    with:
      image: ${{ needs.build.outputs.image }}
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

Verifying SLSA Provenance with slsa-verifier

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Install slsa-verifier
curl -Lo slsa-verifier \
  "https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64"
chmod +x slsa-verifier
sudo mv slsa-verifier /usr/local/bin/

# Verify a container image's provenance
slsa-verifier verify-image \
  ghcr.io/myorg/myrepo:v1.2.3 \
  --source-uri github.com/myorg/myrepo \
  --source-tag v1.2.3

# Output if valid:
# PASSED: Verified SLSA provenance
# {
#   "slsaVersion": "1.0",
#   "builder": { "id": "https://github.com/slsa-framework/..." },
#   "sourceURI": "github.com/myorg/myrepo",
#   "sourceTag": "v1.2.3"
# }

# Verify a binary artifact
slsa-verifier verify-artifact myapp-linux-amd64 \
  --provenance-path myapp-linux-amd64.intoto.jsonl \
  --source-uri github.com/myorg/myrepo

Policy Enforcement with Sigstore’s Policy Controller

Having signatures and provenance is useful only if you enforce their presence. The Sigstore Policy Controller is a Kubernetes admission webhook that blocks unsigned or unverified images from running.

1
2
3
4
5
# Install with Helm
helm repo add sigstore https://sigstore.github.io/helm-charts
helm install policy-controller sigstore/policy-controller \
  --namespace cosign-system \
  --create-namespace
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# ClusterImagePolicy — require all images in the "production" namespace
# to be signed by our GitHub Actions workflow
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-images
spec:
  images:
  - glob: "ghcr.io/myorg/**"
  authorities:
  - keyless:
      url: https://fulcio.sigstore.dev
      identities:
      - issuer: https://token.actions.githubusercontent.com
        subjectRegExp: "https://github.com/myorg/.*/.github/workflows/build.yml@refs/heads/main"
    attestations:
    - name: must-have-sbom
      predicateType: https://spdx.dev/Document
    - name: must-have-slsa
      predicateType: https://slsa.dev/provenance/v0.2
1
2
3
4
5
6
7
# Label the namespace to enforce the policy
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    policy.sigstore.dev/include: "true"

Now any pod in the production namespace that tries to use an unsigned image gets rejected at admission time:

Error from server: admission webhook "policy.sigstore.dev" denied the request:
  image ghcr.io/myorg/myrepo:latest is not signed

Dependency Review: Catching Problems at PR Time

GitHub’s dependency-review-action scans incoming changes to lock files and flags newly introduced vulnerable dependencies before they merge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# .github/workflows/dependency-review.yml
name: Dependency Review

on: [pull_request]

permissions:
  contents: read
  pull-requests: write

jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high
          allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC
          deny-licenses: GPL-3.0, AGPL-3.0    # block copyleft if that's your policy
          comment-summary-in-pr: always

Renovate / Dependabot: Keeping Dependencies Fresh

Automation to keep dependencies updated means CVEs get patched promptly without manual effort:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      production-dependencies:
        dependency-type: "production"
    ignore:
      - dependency-name: "lodash"
        versions: ["4.x"]   # pin specific versions if needed

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Putting It All Together: A Complete Pipeline

A comprehensive supply chain security pipeline in GitHub Actions:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# .github/workflows/secure-build.yml
name: Secure Build Pipeline

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

permissions:
  contents: read
  id-token: write
  packages: write
  security-events: write
  pull-requests: write

jobs:
  # 1. Dependency review on PRs
  dependency-review:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high

  # 2. Build
  build:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          push: ${{ github.event_name != 'pull_request' }}
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

  # 3. Generate and attach SBOM
  sbom:
    needs: build
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: sigstore/cosign-installer@v3
      - uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
          artifact-name: sbom.spdx.json
          output-file: sbom.spdx.json

      - name: Scan SBOM for CVEs
        uses: anchore/scan-action@v3
        with:
          sbom: sbom.spdx.json
          fail-build: true
          severity-cutoff: critical
          output-format: sarif
          output-file: grype.sarif

      - name: Upload scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: grype.sarif

      - name: Attach SBOM attestation
        run: |
          cosign attest --yes \
            --predicate sbom.spdx.json \
            --type spdxjson \
            ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}

  # 4. Sign the image
  sign:
    needs: [build, sbom]
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: sigstore/cosign-installer@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Sign image
        run: |
          cosign sign --yes \
            ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}

  # 5. SLSA provenance (separate workflow call for Level 3)
  provenance:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    permissions:
      actions: read
      id-token: write
      packages: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
    with:
      image: ghcr.io/${{ github.repository }}
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

Quick Reference

Tool Purpose Key Command
Syft Generate SBOMs syft image:latest -o cyclonedx-json
Grype Scan SBOMs for CVEs grype sbom:sbom.json --fail-on high
Cosign Sign/verify images & blobs cosign sign --yes image@digest
cosign verify Verify signature cosign verify --certificate-identity-regexp ... image
cosign attest Attach signed attestation cosign attest --predicate sbom.json --type spdxjson image
slsa-verifier Verify SLSA provenance slsa-verifier verify-image image --source-uri github.com/org/repo
Rekor Query transparency log rekor-cli search --email user@example.com
Policy Controller Enforce signatures in K8s ClusterImagePolicy CRD

The Practical Roadmap

You don’t need to implement everything at once. A sensible progression:

Week 1 — Visibility: Add Syft to your CI pipeline. Generate SBOMs for your most important images and scan them with Grype. Just knowing what’s in your software is valuable before you act on it.

Week 2 — Scanning gates: Add --fail-on critical to Grype scans and block builds that ship critical CVEs. Add dependency-review to your PR workflow.

Week 3 — Signing: Add Cosign keyless signing to your main build workflow. Start verifying signatures in your deployment scripts.

Month 2 — Policy enforcement: Deploy the Sigstore Policy Controller to your staging cluster. Require signatures before images can run.

Month 3 — Provenance: Adopt the SLSA GitHub Generator for your release workflow. Attach SBOM attestations alongside signatures.

Each step compounds the previous. By the end you have: a complete inventory of everything you ship, CVE gating at build time, cryptographic proof of who built each artifact and from which exact source commit, and runtime enforcement that unsigned images simply cannot run. That’s a supply chain you can actually defend.

Comments