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.
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.
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.