Plate 11
After xz: A Practical Supply-Chain Checklist for Solo and Small Teams
Turn the xz-utils backdoor lesson into action: tarball vs git diffs, SBOM, Sigstore Cosign, SLSA provenance, and a solo/small-team release gate you can run this week.
Aditya Challa9 min read
On this page
- Intro — what this post promises
- What xz taught that checklists must capture
- The artifact is not the git tree
- Social trust is not a control
- Detection still matters
- Threat map — map controls to failure modes
- Checklist stage 1 — consuming dependencies
- Pin by content, not by floating tag
- Diff the tarball (non-optional after xz)
- Generate an SBOM on every release candidate
- Checklist stage 2 — building with provenance in mind
- Prefer a hosted builder you do not personally admin
- Sign with Sigstore Cosign (keyless when possible)
- Checklist stage 3 — release and deploy gates
- What this checklist does _not_ claim
- Worked mini-lab (ran 29 Sep 2026 IST)
- More reading
- Stay in touch
Intro — what this post promises
In March 2024, malicious code in XZ Utils 5.6.0 and 5.6.1 (tracked as CVE-2024-3094) showed how a patient supply-chain implant can hitch a ride through a trusted compression library into SSH-adjacent code paths. CISA’s alert recommended downgrading to an uncompromised release (for example 5.4.6 Stable), hunting for related activity, and reporting findings.
Related links:
ShopperCove already covered the story of that backdoor: xz-utils backdoor technical deep dive. This post is deliberately not another narrative retelling. It is the practical checklist you run on your deps and your releases when you do not have a dedicated AppSec team.
Related links:
You will get:
- What xz actually taught practitioners (threat classes, not gossip).
- A staged checklist: consume deps → build → release → deploy.
- Concrete commands for tarball diffs, SBOM, Cosign, and SLSA-oriented provenance — run on a real dependency in lab.
- Honest limits for a solo or three-person team.
Lab honesty: Numbers below are from a ShopperCove lab on 29 Sep 2026 (IST): zlib v1.3.1 (git 51b7f2ab…) release tarball vs git archive; Syft 1.18.1 SBOMs; Cosign v2.4.1 local key sign+verify with a deliberate wrong-key failure. Keyless Cosign hit Sigstore device-flow OIDC and could not complete on this box (no browser identity / no CI id-token). This was one careful afternoon on a shared Linux host — not a full GHCR + admission rollout (that belongs in a dedicated CI workflow lab).
What xz taught that checklists must capture
The artifact is not the git tree
The xz implant was not “just a bad commit someone forgot to review.” Attackers invested in build-time obfuscation and distribution of compromised release artifacts. If your CI only checks out git and never compares what users download from a release page, you miss a whole class of attacks.
Social trust is not a control
Maintainer takeover / multi-year social engineering is outside the reach of npm audit. Your checklist should assume identity of the human can be wrong, and compensate with reproducible process, signed provenance, and artifact comparison.
Detection still matters
Andres Freund’s discovery path (anomalous SSH/CPU behavior during unrelated work) is a reminder: runtime anomaly signals and fuzz/vetting (see our Lily / fuzz-based defense post) complement static gates. Checklist ≠ silver bullet.
Related links:
Threat map — map controls to failure modes
| Failure mode (post-xz lens) | Example | Control you can afford |
|---|---|---|
| Compromised release tarball ≠ git | Hidden build scripts in dist | Diff git archive vs upstream tarball |
| Unsigned / re-tagged container | Registry push from laptop | Cosign sign + admission/CI verify |
| “Provenance” forged on laptop | Fake attestation JSON | Hosted builder + SLSA Build L2+ goals |
| Dependency confusion / typo | Wrong package name | Pin + lockfile + scope allowlist |
| Transitive surprise | New gzip/xz path in tree | SBOM + SCA on every release |
| Silent runtime hook | Backdoor only under rare triggers | Anomaly alerts + staged canaries |
Lab row (zlib v1.3.1): Release tarball had 0 files that were not in git. Four files existed only in git (.github workflows + .gitignore). All 253 shared files were byte-identical (cmp). Empty extras is a valid result — document it and keep the playbook.
Checklist stage 1 — consuming dependencies
Pin by content, not by floating tag
- Lockfiles committed (
go.sum,package-lock.json,Cargo.lock,poetry.lock, …). - Prefer digest pins for container base images (
image@sha256:…) over mutable tags. - Record upstream release URL + checksum for critical native libs.
Diff the tarball (non-optional after xz)
Pattern we ran on zlib v1.3.1:
What we found:
| Signal | zlib v1.3.1 lab |
|---|---|
Files in git archive | 257 |
| Files in release tarball | 253 |
| Only in tarball | 0 |
| Only in git | .github/workflows/{cmake,configure,fuzz}.yml, .gitignore |
| Content mismatches on common files | 0 |
Allowlist lesson: zlib’s maintainer release is a subset of the tagged tree (CI metadata stripped). That is the opposite of the xz-class failure (hidden extras in the dist). Still run the diff — an empty extras list is publishable evidence, not a skipped control.
Trap: GitHub’s auto-generated source archive (archive/refs/tags/v1.3.1.tar.gz) is a different blob (SHA-256 17e88863… in our lab) and still contains the CI files. Diff against the release asset distributors actually fetch, not the auto-archive.
Wall time once tools were present: about two minutes for clone + download + diff on this dependency.
Generate an SBOM on every release candidate
We used Syft 1.18.1. Inventory is the goal — not theater.
Store the SBOM next to the artifact you ship. An empty component list on a bare C tree is honest; the Go mini-service shows what a lockfile-backed release candidate looks like under the same tool.
Checklist stage 2 — building with provenance in mind
Prefer a hosted builder you do not personally admin
SLSA v1.0 Build track ladders roughly as:
Related links:
| Level | Intent (short) |
|---|---|
| Build L1 | Provenance exists (easy to forge) |
| Build L2 | Signed provenance from a hosted build platform |
| Build L3 | Hardened platform: builds isolated; signing material not available to user steps |
Solo teams rarely “finish” L3 on day one. Aim first for L2-shaped habits: builds on GitHub Actions / GitLab CI / Google Cloud Build (or equivalent), provenance attached, verification in the same pipeline that ships.
Sign with Sigstore Cosign (keyless when possible)
Sigstore’s Cosign overview explains identity-based (“keyless”) signing: Fulcio issues a short-lived certificate binding an ephemeral key to an OIDC identity; Rekor records the signing event in a transparency log. That removes long-lived signing keys from your laptop’s ~/.ssh graveyard.
Related links:
Lab — keyless limit (honest): cosign sign-blob --yes on the zlib tarball entered Sigstore’s device-flow OIDC (oauth2.sigstore.dev). This lab box has no browser session and no GitHub Actions id-token, so keyless could not complete. In CI, use something in this shape (pin action SHAs in production):
Lab — local key sign + verify (completed with Cosign v2.4.1):
We also signed the SPDX SBOM and a tiny Go binary the same way — three Verified OK results, one wrong-key exit 1. Verification mindset from Sigstore verify docs: check identity (who) and issuer (which OIDC provider) for keyless; for local keys, treat key distribution as the trust root you are trying to eliminate in production.
Related links:
Checklist stage 3 — release and deploy gates
Minimum gate for anything internet-facing:
- CI built from a protected branch / required reviews
- SBOM artifact uploaded with the release
- Image/binary signed; verify step fails the job on mismatch
- Provenance attestation present for the production digest
- Deploy path rejects unsigned digests (cluster admission policy or CD verify step if you are not on Kubernetes)
- Dependency diff / advisory check for critical native libs this quarter
Measured time on this lab afternoon (not a multi-day CI migration):
| Activity | This lab |
|---|---|
| Install Syft 1.18.1 + Cosign v2.4.1 | ~3 seconds |
| One zlib tarball vs git playbook | ~2 minutes |
| Syft SBOM (zlib tree + Go mini-svc) | ~10 seconds |
| Cosign keyless | blocked (device-flow OIDC; needs CI id-token or browser) |
| Cosign local sign + 3× verify OK + wrong-key fail | ~13 seconds after keys exist |
| Wire Cosign in CI / admission deny | not timed here — natural follow-on for a dedicated workflow lab |
Steady-state costs shrink once the playbook is scripted; do not treat the table above as a week-long AppSec hire substitute.
What this checklist does not claim
| Claim to avoid | Why |
|---|---|
| “SLSA L3 means unhackable” | Levels reduce classes of tampering; they do not erase insider or zero-day risk (SLSA levels). |
| “Cosign replaces code review” | Signatures prove who built what under which identity — not that the code is correct. |
| “SBOM finds backdoors” | SBOMs inventory components; they do not semantically analyze build scripts. Our bare zlib tree correctly produced an empty component list. |
| “We would have caught xz automatically” | Honesty: many small teams would not. The checklist raises the cost for the next attack. Our zlib diff showed no extras — the control still matters for the next dependency that does. |
Related links:
Worked mini-lab (ran 29 Sep 2026 IST)
Scenario: Critical native dep (zlib v1.3.1) consumed via release tarball; tiny Go service as a stand-in release candidate for SBOM + Cosign.
| Step | Signal in lab | Status |
|---|---|---|
Tarball vs git archive | 0 extras; 4 git-only CI files; 253 identical commons | Done |
| Syft SBOM | zlib tree empty; mini-svc: uuid v1.6.0 + stdlib go1.24.4 | Done (Syft 1.18.1) |
| Cosign keyless sign | Device-flow OIDC; no identity on lab box | Blocked — documented |
| Cosign local sign + verify | 3× Verified OK; Rekor indices recorded | Done (v2.4.1) |
| Verify with wrong key | exit 1 | Done |
| Deploy with unsigned digest | Not exercised (no cluster / no GHCR push this lab) | Deferred |
More reading
- xz-utils backdoor technical deep dive
- Not Git Yard / fuzz-based defense
- About ShopperCove
- CISA alert on xz-utils / CVE-2024-3094
- SLSA Build levels
- Cosign signing overview
- Cosign verify
- zlib v1.3.1 release
- Syft
Stay in touch
- Subscribe via RSS: https://www.shoppercove.com/feed.xml
- About the author / method: https://www.shoppercove.com/about
Lab evidence
What I found running this
Lab 29 Sep 2026 IST. zlib v1.3.1: tarball vs git 0 tarball-only extras, 4 git-only (.github/.gitignore), 253/253 common files identical. GitHub auto-archive SHA ≠ release asset. Syft 1.18.1: bare zlib tree empty components; Go mini-svc SBOM uuid v1.6.0 + stdlib go1.24.4. Cosign v2.4.1 keyless blocked (OIDC); local key 3× Verified OK + wrong-key fail.
Related links
Plate 13
Cosign + SBOM in CI: Sign and Attest a Container Image in One Workflow
One CI shape: build an image by digest, Syft SBOM, Cosign sign + SBOM attest, verify success, and prove wrong-key/unsigned failure — the operational follow-on to after-xz.
29 Sept 2026
Plate 80
Lily in CI: Trying Fuzz-Based Backdoor Detection on a Real Repo
Hands-on Lily (ASE 2026) on an owned C toy: rosa/lily 0.6.0 pin, directed catch of a localhost-bind trigger, clean refactor with zero flags, discovery miss, and CI cost math.
30 Sept 2026
Plate 41
React 19.3 View Transitions & Fragment Refs: Frontend Guide (2026)
2 Oct 2026