Adding SLSA Provenance to Package Releases
Provenance answers a question no version number or integrity hash can: was this exact tarball built from the source it claims, by the pipeline it claims? SLSA provenance attestations bind a published npm version to the specific commit, workflow, and runner that produced it, signed through a transparency log. This guide shows how to emit provenance from a GitHub Actions release, what build levels mean, and how consumers verify the result.
Exact Symptom
Without provenance, a consumer auditing your package has no cryptographic way to connect the registry tarball back to your source. Running the verification command on an unattested package reports nothing to verify:
$ npm audit signatures
audited 1 package in 0.6s
1 package has a verified registry signature
# (registry signature only — no provenance, no source binding)
The goal state, after this guide, looks like:
$ npm audit signatures
audited 1 package in 0.7s
1 package has a verified registry signature
1 package has a verified attestation
and the package page shows a "Built and signed on GitHub Actions" provenance badge.
Root Cause Analysis
A registry signature only proves the registry served the bytes it stored; it says nothing about where those bytes came from. An attacker with a stolen publish token can push a trojaned tarball that the registry will sign just as happily as a legitimate one. SLSA (Supply-chain Levels for Software Artifacts) defines build-integrity levels, and provenance is the attestation that raises you above the baseline by tying the artifact to its build. npm provenance uses OpenID Connect: the CI job proves its identity to a Sigstore signing service without any stored key, then records the source commit, repository, and workflow into a signed attestation logged in a public transparency log. This is the publish-side counterpart to the consume-side controls in Supply-Chain Security Hardening, and it builds directly on the OIDC publish pipeline described in Setting Up npm Provenance with GitHub Actions.
SLSA Build Levels at a Glance
| Level | Guarantee | What it requires |
|---|---|---|
| L0 | None | No provenance; trust is implicit. |
| L1 | Provenance exists | Build emits a provenance attestation describing how the artifact was produced. |
| L2 | Signed provenance | Provenance is signed by a hosted build service, resisting tampering after the fact. |
| L3 | Hardened build | Build runs in an isolated, non-falsifiable environment; provenance cannot be forged even by the project's own maintainers. |
npm provenance via GitHub Actions hosted runners reaches roughly SLSA L2 out of the box — signed provenance from a hosted service — which is a large jump over the L0 baseline most packages ship at.
Resolution & Configuration
Follow these steps to publish with provenance.
-
Require Node.js 18+ and a recent npm. Provenance requires npm 9.5+ (
npm --version). Upgrade the CI runner's npm if needed. -
Confirm package metadata points at your source repository. The attestation records your
repositoryfield; it must match the repo the workflow runs in, or publishing rejects the provenance claim.{ "name": "@yourco/widget", "version": "2.1.0", "repository": { "type": "git", "url": "git+https://github.com/yourco/widget.git" } } -
Grant the workflow the
id-tokenpermission. This lets the job mint an OIDC token; without it, provenance generation fails.permissions: contents: read id-token: write # required for OIDC-backed provenance -
Publish with the
--provenanceflag. On a public package from a supported CI provider, npm generates and uploads the attestation automatically.npm publish --provenance --access public -
Use a build provenance action for non-npm artifacts (optional). If you also ship release tarballs or container images, attach build attestations with
actions/attest-build-provenanceso those artifacts carry the same guarantee.
Release Workflow
A complete provenance-emitting release workflow, triggered on a version tag:
# .github/workflows/release.yml
name: Release with Provenance
on:
push:
tags: ['v*']
permissions:
contents: read
id-token: write # mint OIDC token for keyless signing
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# Deterministic install, no third-party scripts during build
- run: npm ci --ignore-scripts
- run: npm run build
# Emits a signed SLSA provenance attestation alongside the tarball
- name: Publish with provenance
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The --provenance flag works whether you authenticate with a token (as above) or with full OIDC publishing; the id-token: write permission is what enables the attestation regardless.
Validation
Confirm the attestation exists and verifies, both as the publisher and as a consumer:
# As a consumer: verify registry signature AND provenance attestation
npm audit signatures
# Inspect the published attestation metadata for a version
npm view @yourco/widget dist.attestations
# Fresh-install check: provenance should resolve without warnings
npm install @yourco/widget && npm audit signatures
A verified result reports 1 package has a verified attestation, and the package's registry page displays the provenance badge linking back to the exact workflow run and commit.
Prevention & Guardrails
- Keep
id-token: writescoped to the publish job only; never grant it repo-wide. - Run
npm ci --ignore-scriptsbefore the build so the attested artifact was produced from a clean, script-free install. - Ensure the
repositoryfield matches the publishing repo exactly, or provenance generation will be rejected. - Trigger releases from tags or releases, not arbitrary pushes, so every attestation corresponds to an intentional version.
- Add
npm audit signaturesto consumers' CI so a missing or invalid attestation on a dependency is caught downstream. - Combine provenance with 2FA-for-writes and short-lived tokens; provenance proves origin but does not by itself stop a stolen token from publishing.
What SLSA provenance proves and what it doesn't
SLSA (Supply-chain Levels for Software Artifacts) provenance is a signed attestation that binds a published artifact to the exact source commit and build process that produced it. Generated by CI through OIDC, it lets a consumer verify that your tarball really came from your repository, built by your workflow, unmodified — closing the gap between the code they can read and the artifact the registry serves. Without it, a hijacked account or a tampered publish can ship anything under your name with no way for a consumer to detect the substitution.
What provenance does not prove is that the code is benign. It confirms origin and integrity — this artifact came from that source and build — not intent, so a malicious-but-authentic author can still publish signed, provenance-backed malware. Provenance defends against a specific and important class of attack: account compromise and supply-chain tampering, where the code you audited is not the code that ships. It is one layer of a broader posture, paired with audit thresholds, host allow-listing, and script blocking, each covering a surface the others do not. Understanding this scope is what lets you rely on provenance for the right guarantee — verifiable origin — rather than mistaking it for a proof of safety it does not provide.
Enabling provenance in the release pipeline
Adding SLSA provenance to a release is usually a small change to the publish workflow: grant it the id-token permission so it can obtain an OIDC token, and publish with the provenance flag so the registry generates and stores the attestation.
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for OIDC provenance
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, registry-url: https://registry.npmjs.org }
- run: npm ci --ignore-scripts
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Publishing must happen from a supported CI provider, because the attestation is only trustworthy if a trusted authority vouches for the build identity — a laptop cannot produce verifiable provenance. Setting the package's repository field correctly ensures the attestation binds to the right source. Once enabled, consumers verify the provenance with npm audit signatures, which confirms the artifact's source and build match its claims. The same OIDC identity that signs the provenance can also authorize the publish, so enabling provenance often goes hand in hand with removing the standing publish token — two supply-chain improvements from one change.
Provenance in a layered supply-chain posture
Provenance is one control in a layered defense, and it is most effective placed alongside the others rather than treated as sufficient on its own. Blocking install scripts stops arbitrary install-time code; an audit threshold catches known-vulnerable versions; host allow-listing with lockfile-lint blocks resolution from untrusted registries; and provenance verification confirms a package came from its claimed source. Each covers a distinct surface, so a package could pass three and fail the fourth — audited-clean but tampered is exactly what provenance catches.
Structured as one CI security gate — a frozen install with ignored scripts, a thresholded audit, a host allow-list check, and npm audit signatures for provenance — these checks measure every push against the whole posture, each fast and deterministic. Provenance's specific contribution is the origin dimension: it catches a hijacked account or a tampered publish that the vulnerability, host, and script checks do not address. For your own packages, publishing provenance is the counterpart to verifying it on your dependencies — you give consumers the means to verify you as you verify the packages you install. The more of the ecosystem that both publishes and verifies provenance, the harder it becomes for a supply-chain substitution to go undetected anywhere in the graph, which is why provenance is worth adopting even though it is one layer rather than a complete defense.
Frequently Asked Questions
What is the difference between a registry signature and a provenance attestation? A registry signature proves the registry served the exact bytes it stored. A provenance attestation proves those bytes were built from a specific source commit by a specific workflow. Provenance is the stronger claim because it ties the artifact to its origin; a stolen publish token can obtain a valid registry signature but cannot forge provenance from your real pipeline.
Do I need provenance if I already use npm provenance via OIDC publishing?
They are the same mechanism. OIDC publishing and --provenance both rely on the id-token: write permission to mint a keyless signing identity. Setting up the OIDC publish flow, detailed in the related provenance setup guide, is what makes the --provenance flag able to emit a signed attestation.
Can private packages get provenance?
Provenance is designed for public packages, since the transparency log and attestation are publicly verifiable. For private packages the value is reduced because consumers are limited, but you can still attach build attestations to release artifacts with actions/attest-build-provenance for internal verification.
What SLSA level does GitHub Actions provenance achieve? Roughly SLSA Build Level 2: the provenance is signed by a hosted build service (Sigstore via GitHub's OIDC), which resists post-hoc tampering. Reaching L3 requires a hardened, isolated build environment where even maintainers cannot forge provenance; L2 is already a substantial improvement over the unattested L0 baseline.
What does SLSA provenance actually prove?
That a published artifact came from a specific source commit and build workflow, unmodified — a verifiable link between your repository and the tarball the registry serves. It defends against account compromise and tampering, but not against a malicious author, since it proves origin and integrity, not that the code is safe.
How do I add provenance to my npm releases?
Grant the publish workflow id-token: write and run npm publish --provenance from a supported CI provider. The registry generates and stores the attestation, and consumers verify it with npm audit signatures. Set the repository field so the attestation binds to the right source.
Why can't I generate provenance from my laptop?
Provenance requires a trusted CI provider that can issue an OIDC identity for the build; a laptop has no authority to vouch for who ran it, so there is nothing trustworthy to sign the attestation. Publish from CI (GitHub Actions, etc.) with the id-token permission.
Do I need provenance if I already run npm audit?
Yes — they cover different surfaces. npm audit catches known-vulnerable versions; provenance catches a tampered or hijacked publish where the code that ships is not the code you audited. Run both, plus script blocking and host allow-listing, as layers of one supply-chain gate.
What SLSA level does npm provenance provide?
npm's provenance corresponds to SLSA build level 2 — it attests the build ran on a trusted, hosted CI platform and links the artifact to its source and workflow. It does not by itself reach the higher levels that require a hardened, isolated build environment, but level 2 already closes the account-hijack and tampering gap for most packages.
Related
- Setting Up npm Provenance with GitHub Actions — the OIDC publish pipeline this provenance flow builds on.
- Enforcing npm audit Thresholds in CI —
npm audit signaturesis also where consumers verify attestations. - Configuring lockfile-lint for Supply-Chain Safety — the consume-side integrity gate that pairs with publish-side provenance.