SLSA Provenance & Supply-Chain Integrity
███████╗██╗ ███████╗ █████╗
██╔════╝██║ ██╔════╝██╔══██╗
███████╗██║ ███████╗███████║
╚════██║██║ ╚════██║██╔══██║
███████║███████╗███████║██║ ██║
╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝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.1 also defines a Source track (commit/review integrity). The L1–L3 most people mean are Build-track. This guide targets the Build track.
The L2 → L3 jump is a trust-model choice
- L2 says "I trust each developer." The platform signs from its control plane, but the build environment is tenant-controlled — anyone who can influence the pipeline can influence the build.
- L3 says "I trust the platform, not the tenant." Provenance is generated in an isolated control plane, structurally resistant to forgery by build steps.
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:
private: false(or publish to a private registry), plusrepository,license, andfilesfields so only intended files ship.- An npm automation token stored as the
NPM_TOKENrepo secret. - A version bump + a
v*git tag to trigger the release.
.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.Ztag (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 ReleaseVerification (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 statusRelease 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.0
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.3verify-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:
- dependency review / lockfile pinning,
- secret scanning (this repo's
gitleaksworkflow), - code review and the SLSA Source track.
It defeats substitution attacks (dependency confusion, tampered artifacts, impostor publishers) — which is real, high-value coverage — not insider attacks.
codeAmani policy (planning & build integration)
- 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.
- 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. - Always-loaded policy. The summary rubric lives in the root
CLAUDE.md(## Supply-Chain & Provenance (SLSA)), so every Claude Code session inherits it. - 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
- SLSA spec v1.1 — https://slsa.dev/spec/v1.1/
- L2 vs L3 / build isolation — https://slsa.dev/blog/2026/05/mini-shai-hulud-what-slsa-can-and-cannot-do
- Node.js SLSA3 builder — https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/nodejs/README.md
- Generic SLSA3 generator — https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md
- slsa-verifier — https://github.com/slsa-framework/slsa-verifier
- npm provenance — https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/
- Boundary analysis (OpenSSF, 2026) — https://openssf.org/blog/2026/06/10/mini-shai-hulud-where-slsas-boundaries-fall/