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.
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 dataSupply 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:
- Run in an isolated, ephemeral environment (no persistent build agents with write access)
- Generate provenance inside the build system (not by the build script itself)
- The provenance must be signed by the build system's key, not a user key
- All build inputs (source, dependencies) must be pinned (no
latesttags, hash-pinned)
GitHub Actions provides SLSA 3 provenance via the slsa-framework/slsa-github-generator action:
# .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 discardedSigning in CI
# .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 modeThe --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:
cosign verify \
--certificate-identity-regexp="https://github.com/org/service" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/org/service:latestSBOM: 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
# 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.jsonGenerating SBOMs with Trivy
# 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:latestAttaching SBOM to the image with Cosign
# Attach the SBOM as a signed attestation to the image
cosign attest \
--yes \
--predicate sbom.cyclonedx.json \
--type cyclonedx \
ghcr.io/org/service@$DIGESTThis 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
- 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 provenancecyclonedx— SBOMvuln— vulnerability scan resultssarif— static analysis resultscustom— any predicate type
{
"_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
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.devAny Pod spec referencing an unsigned image from ghcr.io/org/* will be rejected at admission.
Require SBOM attestation
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)
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:
helm install trivy-operator aquasecurity/trivy-operator \
--namespace trivy-system \
--create-namespace \
--set="trivy.ignoreUnfixed=true"It creates VulnerabilityReport CRDs for every running image:
kubectl get vulnerabilityreports -A
# NAME REPOSITORY SCANNER CRITICAL HIGH MEDIUM
# replicaset-myapp-xyz-myapp ghcr.io/org/myapp Trivy 0 2 8Wire 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?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.