cicd · level 9

Supply-Chain Security

SLSA, sigstore, dependency pinning, and the XZ backdoor.

225 XP

Supply-Chain Security

A modern web service runs on hundreds of dependencies, each maintained by people you've never met, built by systems you don't control. Supply-chain security is the discipline of trusting less and verifying more — across that whole chain, not just your own code.

The threat model

Three categories of supply-chain attack:

  1. Dependency compromise — a package you depend on (or a transitive dep) is compromised. Either the maintainer's account was hijacked, the registry was hacked, or — in the worst version — the maintainer themselves was malicious.
  2. Build-system compromise — your CI provider, build runner, or registry is compromised. The bytes you ship are not the bytes your source produces.
  3. Maintainer takeover — the slowest-burning category. A malicious actor gradually builds trust over years, becomes a maintainer, then plants a backdoor.

The XZ Utils incident in 2024 was category 3, and is the cautionary tale of the decade.

XZ Utils — the cautionary tale

xz is a compression library shipped on roughly every Linux distribution. SSH on those distros linked against liblzma, which depends on xz. In 2024 a maintainer with the username "Jia Tan" — who had been contributing for ~2 years and was a co-maintainer for ~1 — committed a series of changes that, in the right combination, created a backdoor activated by an attacker-supplied SSH public key. The backdoor didn't trigger except on systems linked into SSH, and even then only when the attacker provided the magic key.

The backdoor was discovered by accident: a Microsoft engineer noticed sshd was 500ms slower than usual and dug in. The story would not have ended without that.

What it tells us:

  • The attacker invested years. Mailing-list arguments, accepted patches, gradual handover of trust. This was a state-actor-level operation.
  • Code review failed. The malicious code was hidden in test fixtures (binary .xz files) that build scripts unpacked. Reviewers can't review opaque test fixtures byte-by-byte.
  • The build system was the attack surface. The actual backdoor logic lived in the build's autotools macros, not the source files visible on GitHub.

The structural lessons:

  1. Don't trust binary test fixtures. Insist on plain-text inputs, or recompile fixtures from source.
  2. Reproducible builds matter. If the binary you ship doesn't match what the source produces deterministically, something is wrong.
  3. Code review is necessary but insufficient. Reviewers see source; the exploit hid in built artifacts.
  4. Maintainer trust takes years to gain — and seconds to lose. This pattern will repeat.

SLSA — the framework

SLSA (Supply chain Levels for Software Artifacts) defines four levels of build integrity:

  • L1: documentation of build process, automated provenance generation.
  • L2: builds run on a hosted service (no developer can produce a fake build from a laptop).
  • L3: build environment is hardened — every input recorded, no developer can tamper with the build.
  • L4: full review and reproducibility — two-party review of every change, deterministic builds.

In 2026, with GitHub Actions plus the attest-build-provenance action, you get L3 with no extra work:

- uses: actions/attest-build-provenance@v1
  with:
    subject-name: ghcr.io/me/app
    subject-digest: ${{ digest }}

The attestation lands in Sigstore's Rekor (a public transparency log) and ties the artifact's digest to:

  • The exact source commit it came from.
  • The CI workflow file that built it.
  • The runner environment.
  • The OIDC identity of the build job.

Verifiers check all four:

cosign verify-attestation \
  --type slsaprovenance \
  --certificate-identity-regexp "https://github.com/me/app/.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/me/app@sha256:abc...

If any of those checks fail, the artifact didn't come from where it claims. Don't deploy it.

Sigstore — the trust anchor

Sigstore is an umbrella project that makes the cryptographic plumbing usable:

  • Fulcio — a public Certificate Authority that issues short-lived (~10 min) X.509 certs in exchange for an OIDC token. You don't manage long-lived signing keys.
  • Rekor — a public Merkle-tree transparency log. Every signed artifact is recorded; deletion is impossible.
  • cosign — the CLI / GitHub Action that ties them together.

The flow:

  1. CI job requests OIDC token from GitHub.
  2. cosign sends OIDC token to Fulcio.
  3. Fulcio verifies the token, issues a short-lived cert containing claims (repo, branch, run ID).
  4. cosign signs the artifact's digest with the cert's private key.
  5. Signature + cert are uploaded to Rekor.
  6. Verifier checks the cert chain, the OIDC claims, and the Rekor log entry.

Trust anchor: the OIDC issuer (GitHub, GitLab, Buildkite). If GitHub is compromised, the trust collapses; until then, this is dramatically better than long-lived PGP keys hoarded in a vault.

Dependency pinning

The first line of defense is also the cheapest: pin every dependency to an exact version with an integrity hash.

# package-lock.json — pinned + integrity
"react": {
  "version": "18.3.1",
  "integrity": "sha512-..."
}

# Cargo.lock — pinned with checksums
[[package]]
name = "tokio"
version = "1.39.0"
checksum = "..."

# requirements.txt — exact versions; pip can't enforce hashes without --require-hashes
react==18.3.1

For containers, pin by digest:

FROM node:20-alpine@sha256:abc...     # not :20-alpine; that floats

Tools that automate the pinning + scanning:

  • Dependabot (GitHub-native) — opens PRs for security updates, audits the lockfile.
  • Renovate — broader scope, supports more package managers, configurable cadence.
  • Snyk / Trivy — scans deps for known vulns; emits SARIF for code-review tools.

SBOM — what's in the box

A Software Bill of Materials lists every dependency in a build. It's table stakes for anything shipped to a regulated industry, and a baseline for incident response: when a CVE drops for a particular package, the first question is "are we shipping that?". An SBOM answers it in seconds.

Two main formats: CycloneDX (OWASP), SPDX (Linux Foundation). Both are JSON/XML lists of (package, version, license, hash) tuples. Generators:

# Container image SBOM
syft ghcr.io/me/app:latest -o cyclonedx-json > sbom.cdx.json

# Source-tree SBOM
syft .

Once you have an SBOM, store it next to the artifact in your registry and refer to it during incident response.

Reproducible builds

A build is reproducible if running the same source on a different host (with the same toolchain pinned) produces a byte-identical binary. It's harder than it sounds — timestamps, hostname leaks, parallel-build non-determinism, dynamic linking.

The Reproducible Builds project at reproducible-builds.org catalogues techniques for each language. Debian aims for nearly 100% reproducibility; Bazel is reproducible by design; npm/pip are improving slowly.

Why bother? Because if your binary is reproducible, anyone can verify it was built from the claimed source. The XZ backdoor would have been visible: the official binary wouldn't match the binary that anyone could rebuild from the public source.

Defence-in-depth checklist

For a service that takes supply-chain seriously:

  • All deps pinned to exact versions with integrity hashes
  • Lockfiles enforced in CI (npm ci, pip install --require-hashes, cargo build --frozen)
  • Dependabot or Renovate auto-PRs security updates
  • Trivy / Snyk scans on every PR + scheduled weekly
  • SBOM generated on every build, stored alongside the artifact
  • Container images pinned by digest, not tag
  • cosign signing with sigstore on every release
  • SLSA provenance attestations generated and verified at deploy
  • CI uses OIDC trust to clouds (no long-lived keys)
  • Production deploys verify provenance signatures before rolling out
  • Maintainer review for any dep added; auto-deny new deps without sign-off
  • Sub-resource integrity (SRI) on every external script tag in HTML

This is the 2026 baseline. The XZ incident was the wake-up call; the next one is in someone's editor right now.

Summary

  • Supply-chain attacks are now top-tier threats. Pin, sign, verify, audit.
  • SLSA defines build-integrity levels; sigstore provides the cryptographic plumbing.
  • Reproducible builds let you verify "the binary I have matches the source that produced it".
  • The XZ backdoor (2024) is the case study; it succeeded because of trust — the structural defenses against it are reproducible builds and rigorous review of binary inputs.
  • Treat your dependency tree like production code, because that's what it is.

Tools in the wild

6 tools
  • cosignfree tier

    Sigstore CLI — sign, verify, and attest container images and OCI artifacts.

    cli
  • Syftfree tier

    Generates SBOMs (CycloneDX/SPDX) from container images and source repos.

    cli
  • Trivyfree tier

    Vuln scanner for containers, dependencies, IaC. Runs in CI; outputs SARIF.

    cli
  • Dependabotfree tier

    GitHub-native dep updater + security advisor. Free for public + private repos.

    service
  • Dependency + container vuln scanner with auto-fix PRs and SBOM export.

    service
  • Project + tools for verifying that source-to-binary builds are deterministic.

    spec