Learnixo
Back to blog
Cloud & DevOpsadvanced

Platform Engineering: Software Supply Chain Security — SLSA, Sigstore, SBOM, and Attestations

Deep guide to software supply chain security for platform engineers — SLSA levels, Sigstore keyless signing with Cosign, SBOM generation with Syft and Trivy, in-toto attestations, and enforcing image provenance with Kyverno at Kubernetes admission.

LearnixoJune 9, 20269 min read
Platform EngineeringSupply Chain SecuritySLSASigstoreSBOMKyvernoKubernetesSecurity
Share:𝕏

Why Supply Chain Security Belongs in the Platform

The 2020 SolarWinds attack and the 2021 log4shell vulnerability changed how the industry thinks about software security. The threat isn't just at runtime — it's in the build pipeline, the dependencies, the container images, and the artifacts you ship.

Platform engineers are uniquely positioned to address this because the platform controls:

  • How artifacts are built (CI/CD pipeline)
  • What gets deployed (Kubernetes admission webhooks)
  • What runs in production (image policy enforcement)

A supply chain security program built into the platform is automatic for every team. Built team-by-team, it's inconsistent and incomplete.


The Attack Surface

Understanding what you're protecting:

Source code → Build pipeline → Container image → Registry → Kubernetes cluster → Runtime

Each step is an attack surface:
├── Source code: compromised dependency (SolarWinds injected malware in source)
├── Build pipeline: compromised CI system (builds can be tampered if CI is compromised)
├── Container image: malicious base image, CVE-laden packages
├── Registry: image tampered in transit or at rest
├── Admission: unsigned/untrusted image deployed
└── Runtime: compromised container exfiltrates data

Supply chain security addresses integrity across all these steps — proving that what reached production is exactly what was built from the reviewed source code.


SLSA: Supply-chain Levels for Software Artifacts

SLSA (pronounced "salsa") is a security framework that defines four levels of build integrity assurance:

| Level | Requirement | What It Proves | |-------|-------------|----------------| | SLSA 1 | Provenance exists | Build was documented (who built it, from what source commit) | | SLSA 2 | Signed provenance, hosted build service | Provenance cannot be forged post-build | | SLSA 3 | Non-falsifiable provenance, isolated build | Build process cannot tamper with its own provenance; hermetic build | | SLSA 4 | Two-party review, reproducible | Two humans reviewed every change; bit-for-bit reproducible builds |

Most organizations are SLSA 0-1 today. Getting to SLSA 3 is the meaningful security improvement.

SLSA 3 Requirements in Practice

For SLSA 3, your build system must:

  1. Run in an isolated, ephemeral environment (no persistent build agents with write access)
  2. Generate provenance inside the build system (not by the build script itself)
  3. The provenance must be signed by the build system's key, not a user key
  4. All build inputs (source, dependencies) must be pinned (no latest tags, hash-pinned)

GitHub Actions provides SLSA 3 provenance via the slsa-framework/slsa-github-generator action:

YAML
# .github/workflows/build.yml
jobs:
  build:
    outputs:
      digests: ${{ steps.hash.outputs.digests }}
    steps:
      - name: Build container
        run: |
          docker build -t ghcr.io/org/service:${{ github.sha }} .
          docker push ghcr.io/org/service:${{ github.sha }}

      - name: Output digest
        id: hash
        run: |
          DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/org/service:${{ github.sha }})
          echo "digests=$DIGEST" >> $GITHUB_OUTPUT

  provenance:
    needs: [build]
    permissions:
      actions: read
      id-token: write
      packages: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0
    with:
      image: ghcr.io/org/service
      digest: ${{ needs.build.outputs.digests }}

This generates a signed SLSA 3 provenance attestation attached to the image in the registry.


Sigstore and Cosign: Keyless Image Signing

Signing container images cryptographically proves they haven't been tampered with between the registry and production.

Traditional signing required managing long-lived private keys — a security problem in itself (key rotation, key compromise, key sprawl).

Sigstore's keyless signing uses OIDC identity (GitHub Actions, Google Workload Identity) to sign without storing keys:

GitHub Actions job has OIDC token (identity: "github.com/org/repo")
  → Cosign requests short-lived signing certificate from Fulcio (Sigstore CA)
  → Fulcio verifies OIDC token, issues certificate (valid 10 minutes)
  → Cosign signs the image digest with the ephemeral key
  → Signature + certificate stored in Rekor (immutable transparency log)
  → Ephemeral key discarded

Signing in CI

YAML
# .github/workflows/build.yml
- name: Sign container image
  uses: sigstore/cosign-installer@v3

- name: Sign with keyless OIDC
  run: |
    cosign sign --yes ghcr.io/org/service@${{ steps.build.outputs.digest }}
  env:
    COSIGN_EXPERIMENTAL: "1"   # keyless mode

The --yes flag skips the interactive prompt in CI. COSIGN_EXPERIMENTAL=1 enables the Rekor transparency log.

Verifying signatures

Anyone can verify the signature without a private key:

Bash
cosign verify \
  --certificate-identity-regexp="https://github.com/org/service" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/org/service:latest

SBOM: Software Bill of Materials

An SBOM is a machine-readable list of every package and dependency in your container image — like a nutrition label for software.

Why it matters:

  • When log4shell hit, organizations with SBOMs found affected services in minutes. Without SBOMs, it took weeks.
  • Regulatory requirements (US Executive Order 14028) mandate SBOMs for software sold to the US government
  • Enables continuous CVE scanning against a known dependency inventory

Generating SBOMs with Syft

Bash
# Generate SBOM in SPDX format
syft ghcr.io/org/service:latest -o spdx-json > sbom.json

# Generate in CycloneDX format (better tooling support)
syft ghcr.io/org/service:latest -o cyclonedx-json > sbom.cyclonedx.json

Generating SBOMs with Trivy

Bash
# Trivy generates SBOM and scans CVEs in one pass
trivy image \
  --format cyclonedx \
  --output sbom.json \
  ghcr.io/org/service:latest

# Separate CVE report
trivy image \
  --format json \
  --output cve-report.json \
  --exit-code 1 \         # fail build on CRITICAL CVEs
  --severity CRITICAL,HIGH \
  ghcr.io/org/service:latest

Attaching SBOM to the image with Cosign

Bash
# Attach the SBOM as a signed attestation to the image
cosign attest \
  --yes \
  --predicate sbom.cyclonedx.json \
  --type cyclonedx \
  ghcr.io/org/service@$DIGEST

This stores the SBOM alongside the image in the registry as an OCI artifact, tamper-proof and cryptographically linked to the image digest.

Complete CI pipeline with signing + SBOM

YAML
- name: Build and push image
  id: build
  uses: docker/build-push-action@v5
  with:
    push: true
    tags: ghcr.io/org/service:${{ github.sha }}

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

- name: Generate SBOM
  run: |
    syft ghcr.io/org/service@${{ steps.build.outputs.digest }} \
      -o cyclonedx-json > sbom.json

- name: Scan for CVEs
  run: |
    trivy image \
      --exit-code 1 \
      --severity CRITICAL \
      ghcr.io/org/service@${{ steps.build.outputs.digest }}

- name: Sign image
  run: |
    cosign sign --yes ghcr.io/org/service@${{ steps.build.outputs.digest }}

- name: Attest SBOM
  run: |
    cosign attest \
      --yes \
      --predicate sbom.json \
      --type cyclonedx \
      ghcr.io/org/service@${{ steps.build.outputs.digest }}

in-toto Attestations: Chain of Custody

in-toto is a framework for defining and verifying the entire software supply chain as a policy — not just one step.

An in-toto layout defines:

  • Who performs what step
  • What artifacts are consumed and produced at each step
  • The rules for linking steps together

Attestation types (standardized via in-toto Attestation Framework):

  • slsaprovenance — build provenance
  • cyclonedx — SBOM
  • vuln — vulnerability scan results
  • sarif — static analysis results
  • custom — any predicate type
JSON
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://cyclonedx.org/schema",
  "subject": [
    {
      "name": "ghcr.io/org/service",
      "digest": { "sha256": "abc123..." }
    }
  ],
  "predicate": {
    // SBOM content
  }
}

Attestations linked to image digests create an auditable chain: you can prove that the image in production came from exactly this source commit, was built by this GitHub Actions workflow, has no CRITICAL CVEs (as of build time), and has this exact dependency inventory.


Enforcing Image Provenance with Kyverno

Generating attestations is only half the job. The platform must refuse to deploy unsigned or unverified images.

Require signed images

YAML
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: check-image-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/org/*"
          attestors:
            - count: 1
              entries:
                - keyless:
                    subject: "https://github.com/org/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev

Any Pod spec referencing an unsigned image from ghcr.io/org/* will be rejected at admission.

Require SBOM attestation

YAML
rules:
  - name: check-sbom-attestation
    match:
      any:
        - resources:
            kinds: [Pod]
    verifyImages:
      - imageReferences:
          - "ghcr.io/org/*"
        attestations:
          - predicateType: https://cyclonedx.org/schema
            attestors:
              - entries:
                  - keyless:
                      subject: "https://github.com/org/*"
                      issuer: "https://token.actions.githubusercontent.com"

Require no CRITICAL CVEs (verified by attestation)

YAML
rules:
  - name: block-critical-cves
    match:
      any:
        - resources:
            kinds: [Pod]
    verifyImages:
      - imageReferences:
          - "ghcr.io/org/*"
        attestations:
          - predicateType: cosign.sigstore.dev/attestation/vuln/v1
            conditions:
              - all:
                  - key: "{{ scanner.result.summary.CRITICAL }}"
                    operator: Equals
                    value: "0"

Trivy Operator: Continuous In-Cluster CVE Scanning

Signing images at build time is point-in-time. New CVEs are discovered daily. The Trivy Operator runs continuously inside the cluster:

Bash
helm install trivy-operator aquasecurity/trivy-operator \
  --namespace trivy-system \
  --create-namespace \
  --set="trivy.ignoreUnfixed=true"

It creates VulnerabilityReport CRDs for every running image:

Bash
kubectl get vulnerabilityreports -A
# NAME                            REPOSITORY                SCANNER   CRITICAL  HIGH  MEDIUM
# replicaset-myapp-xyz-myapp     ghcr.io/org/myapp         Trivy     0         2     8

Wire Trivy Operator reports to Prometheus + Grafana to alert when a running service exceeds your CVE SLA (e.g., patch CRITICAL within 24h).


Putting It All Together: The Platform Security Pipeline

Source → PR review → merge → GitHub Actions:
  1. Build image (SHA-tagged, no latest)
  2. Generate SBOM (Syft → CycloneDX)
  3. Scan CVEs (Trivy, fail on CRITICAL)
  4. Run SAST (CodeQL / SonarQube)
  5. Sign image (Cosign keyless, OIDC from GitHub Actions)
  6. Attest SBOM (cosign attest)
  7. Attest SLSA provenance (slsa-github-generator)
  8. Push to registry

Deploy → ArgoCD syncs → Kubernetes admission webhook:
  - Kyverno: verify image signature ✓
  - Kyverno: verify SBOM attestation exists ✓
  - Kyverno: verify no CRITICAL CVEs in attestation ✓
  - Kyverno: image must come from approved registry ✓
  → Pod admitted

Runtime:
  - Trivy Operator scans running images daily
  - VulnerabilityReport → Prometheus alert if new CRITICAL discovered
  - Alert: "Image in production now has CRITICAL CVE — patch within 24h"

The platform provides this pipeline via the golden path template. Every service scaffolded via Backstage gets this automatically. Teams write code; the platform handles the security chain.


Maturity Roadmap

| Stage | What to implement | SLSA Level | |-------|-------------------|-----------| | Start | Image scanning in CI (Trivy), block CRITICAL deploys | 0 → 1 | | Foundation | Cosign image signing, Kyverno admission verification | 1 → 2 | | Advanced | SBOM generation + attestation, SLSA provenance via generator | 2 → 3 | | Elite | In-toto layout policies, reproducible builds, Rekor-verified supply chain | 3 → 4 |

Start with image scanning — it's the highest ROI step. Teams get immediate CVE feedback. Add signing once scanning is stable. Layer SBOM and attestations as the organization matures.

Enjoyed this article?

Explore the Cloud & DevOps learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.