SLSA Provenance & Supply-Chain Integrity

Technology: supply-chain · Category: tooling · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/supply-chain

Insight:

SLSA levels are Build-track assurance levels, not a tool you install. Default any artifact that ships (npm package, release tarball, container) to Build L3 via the isolated builder — npm publish --provenance alone is only L2. Anything that does not ship a downloadable artifact (this KB, Vercel apps) needs no provenance: pin your Actions and document the build instead.

███████╗██╗     ███████╗ █████╗
██╔════╝██║     ██╔════╝██╔══██╗
███████╗██║     ███████╗███████║
╚════██║██║     ╚════██║██╔══██║
███████║███████╗███████║██║  ██║
╚══════╝╚══════╝╚══════╝╚═╝  ╚═╝

SLSA (Supply-chain Levels for Software Artifacts) is an OpenSSF framework for build integrity. It answers one question for whoever installs your artifact: "Here is the verifiable, unforgeable record of how and where this was built." This guide is policy for codeAmani: decide a provenance target at plan time, not at release.


What SLSA is (and isn't)

SLSA is not a package, an action, or a vendor. It is a maturity framework. The famous numbers (L1/L2/L3) are assurance levels on the Build track — each adds a stronger guarantee about how trustworthy an artifact's provenance is.

Level Guarantee Roughly achieved by
Build L1 Provenance exists — an automated build emits a record of how the artifact was made. Falsifiable. A build script + recorded metadata
Build L2 Provenance is signed & authenticated, built on a hosted platform. Binds artifact → source repo + builder. npm publish --provenance from GitHub Actions
Build L3 + Build isolation / non-falsifiable — signing happens in a trusted control plane the build steps cannot reach. slsa-github-generator isolated builder

SLSA v1.2 (the current approved spec, superseding v1.1) formally introduces a Source track (commit/review integrity) alongside the Build track, and updates the threat model to cover it. The L1–L3 most people mean are still Build-track; this guide targets the Build track.

The L2 → L3 jump is a trust-model choice

That is why npm publish --provenance is only L2 — build and signing share one tenant-controlled job. L3 requires the isolated builder below.


Decision rubric — does this project ship an artifact?

This is the only question that matters. SLSA protects distributed artifacts. If nothing is downloaded, there is nothing to attest.

Project archetype Ships a downloadable artifact? Target Recipe
Knowledge base / docs (e.g. the tech-stack repo) No n/a — pin Actions, document build —
Next.js app on Netlify/Vercel (dashboard, mail, kipaji-web) No — a deploy, not an artifact SLSA-aware only hardened CI, no provenance
Published npm package / CLI / MCP server Yes (registry) Build L3 npm isolated builder ↓
GitHub Release tarball Yes (release asset) Build L3 generic generator ↓
Container image Yes (registry) Build L3 container generator

Netlify/Vercel deployments do not emit SLSA3 provenance. To put provenance on a deployed app you would have to build the deployable artifact in GitHub Actions with the generic generator and deploy that attested output — heavy, and it fights Vercel's build-on-push model. Not worth it unless an artifact genuinely ships.


Recipe A — npm package → Build L3

Use the Node.js isolated builder. It builds in a control plane your scripts can't reach, generates non-falsifiable provenance, and the companion publish action pushes the package + provenance to npm.

Prerequisites on the package:

.github/workflows/release-npm-slsa3.yml:

name: release-npm-slsa3
on:
  push:
    tags: ["v*"]

permissions: read-all   # tighten per-job below

jobs:
  build:
    permissions:
      id-token: write   # OIDC token for Sigstore signing
      contents: read    # checkout
      actions: read     # read workflow run metadata
    if: startsWith(github.ref, 'refs/tags/')
    uses: slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml@v2.1.0
    with:
      # In a monorepo, point at the package dir, e.g. packages/mcp-server
      run-scripts: "ci, test, build"   # run INSIDE the isolated builder before packing

  publish:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Set up npm registry auth
        uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: "https://registry.npmjs.org"
      - name: publish (package + provenance)
        uses: slsa-framework/slsa-github-generator/actions/nodejs/publish@v2.1.0
        with:
          access: public
          node-auth-token: ${{ secrets.NPM_TOKEN }}
          package-name: ${{ needs.build.outputs.package-name }}
          package-download-name: ${{ needs.build.outputs.package-download-name }}
          package-download-sha256: ${{ needs.build.outputs.package-download-sha256 }}
          provenance-name: ${{ needs.build.outputs.provenance-name }}
          provenance-download-name: ${{ needs.build.outputs.provenance-download-name }}
          provenance-download-sha256: ${{ needs.build.outputs.provenance-download-sha256 }}

The reusable workflow must be pinned to a @vX.Y.Z tag (not a branch or short SHA) — the generator refuses to run otherwise. Pin your other actions (setup-node, checkout) to full commit SHAs per house security policy.


Recipe B — GitHub Release tarball → Build L3

When you ship a downloadable asset (not a registry package), build it yourself, then hand the subjects (sha256 of each artifact, base64-encoded) to the generic generator, which signs and attaches .intoto.jsonl provenance to the Release.

.github/workflows/release-tarball-slsa3.yml:

name: release-tarball-slsa3
on:
  push:
    tags: ["v*"]

permissions: read-all

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      hashes: ${{ steps.hash.outputs.hashes }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci && npm run build
      - name: pack artifact
        run: tar -czf dist.tar.gz dist/
      - name: compute subjects
        id: hash
        run: echo "hashes=$(sha256sum dist.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT"

  provenance:
    needs: [build]
    permissions:
      actions: read     # detect the Actions environment
      id-token: write   # Sigstore signing
      contents: write   # upload provenance to the Release
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
    with:
      base64-subjects: "${{ needs.build.outputs.hashes }}"
      upload-assets: true   # attach <artifact>.intoto.jsonl to the Release

Recipe C — GitHub artifact attestations (lighter, ≈ L2)

When the full isolated builder is more ceremony than you want, GitHub's own actions/attest-build-provenance (currently v4 — as of v4 it is a thin wrapper over actions/attest) emits a signed SLSA build-provenance attestation for any artifact in one step, signed via Sigstore (the public-good instance for public repos, GitHub's private instance for private/internal repos) and stored in GitHub's attestations API.

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write     # Sigstore/OIDC signing
      contents: read
      attestations: write # write to the attestations API
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build && tar -czf dist.tar.gz dist/
      - uses: actions/attest-build-provenance@v4
        with:
          subject-path: dist.tar.gz

Verify on the consumer side with the GitHub CLI:

gh attestation verify dist.tar.gz --repo codeAmani-Solutions/<repo>

This is L2-class, not L3. Provenance is generated in the same tenant-controlled job that runs the build — there is no build isolation, so it carries the same L2 trust model as npm publish --provenance. Reach for it when you want signed, verifiable provenance with minimal wiring; use the isolated builder (Recipe A/B) when the artifact genuinely warrants L3.


Verification (the half people skip)

Provenance that nobody verifies buys nothing. Two consumer-side paths:

npm packages — simplest is the npm CLI after install:

npm audit signatures        # reports registry signature + provenance attestation status

Release tarballs / generic artifacts — use slsa-verifier:

# install (Go) — or grab a release binary
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@v2.7.1

slsa-verifier verify-artifact dist.tar.gz \
  --provenance-path dist.tar.gz.intoto.jsonl \
  --source-uri github.com/codeAmani-Solutions/<repo> \
  --source-tag v1.2.3

verify-artifact fails unless the artifact's digest, the source repo, and the builder identity all match — this is what makes a forged or swapped artifact detectable.

CI gate: for any dependency that publishes provenance, add a verify step in CI so an unverifiable build fails rather than silently proceeding.


What SLSA3 does NOT do (be honest about the boundary)

SLSA attests build integrity, not source benevolence. As of 2026 (see the OpenSSF "Mini Shai-Hulud" analysis), L3 provenance will faithfully sign an artifact built from malicious source or a compromised dependency — it proves the how/where, not that the code is safe. Provenance complements, does not replace:

It defeats substitution attacks (dependency confusion, tampered artifacts, impostor publishers) — which is real, high-value coverage — not insider attacks.


codeAmani policy (planning & build integration)

  1. Plan-time decision. Every new project's plan records a Provenance target using the rubric above. Default for anything that ships an artifact: Build L3.
  2. Template-scaffolded. New shippable projects inherit a commented release workflow from codeAmani-labs-projects/_TEMPLATE-PROJECT/.github/workflows/release-slsa3.yml — enable it on first publish; zero retrofit.
  3. Always-loaded policy. The summary rubric lives in the root CLAUDE.md (## Supply-Chain & Provenance (SLSA)), so every Claude Code session inherits it.
  4. Freshness-tracked. The docs: URLs above are watched by the tech-stack checker; when the builder ships a new major (v2 → v3) this guide is flagged for review.

References

Official docs: