Without provenance, a consumer installing your package has no cryptographic way to confirm it was built from the source they can read on GitHub — they are trusting the maintainer’s account, the registry, and every machine in between. This matters most for dual-format packages distributed through automated pipelines: the build step compiles both ESM and CJS artifacts, and provenance is what proves those artifacts came from the CI run that ran your published source, not a modified local build.

Prerequisites


Canonical Configuration Block

The minimal addition to an existing release workflow is the id-token: write permission and the --provenance flag — nothing else about the publish command changes:

name: Publish with Provenance
on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  id-token: write   # required — grants the OIDC token Sigstore needs to sign

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'
      - run: npm ci
      - run: npm run build
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

id-token: write is the permission GitHub grants a workflow to mint a short-lived OIDC token scoped to that specific run. npm’s CLI uses this token internally — it never appears in your logs or environment as a usable credential on its own.


Provenance Signing Flow

Sigstore Provenance Signing Flow A left-to-right flow showing the GitHub Actions runner requesting an OIDC token, exchanging it with Sigstore's Fulcio authority for a short-lived signing certificate, signing the tarball digest, and recording the attestation in the Rekor transparency log before the registry stores it against the published version. GitHub Actions runner (id-token: write) OIDC token short-lived, run-scoped Fulcio CA issues signing cert npm CLI signs digest Rekor transparency log public, append-only entry npm registry stores attestation link Consumers verify with: npm audit signatures

Step-by-Step Implementation

Step 1 — Enable the id-token permission

Add the permission at the workflow or job level. Job-level scoping is preferable when other jobs in the same workflow do not need it:

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write

Expected result: the runner can request an OIDC token from GitHub’s identity provider during the job; no visible output changes until the publish step runs.

HAZARD PREVENTION

Symptom: npm publish --provenance fails with an error indicating no OIDC token is available, even though the workflow ran successfully.

Root cause: id-token: write was set on an unrelated job, or on a reusable workflow that does not propagate permissions to the caller.

Fix: Confirm the permission is declared on the exact job that runs npm publish, and that any reusable workflow explicitly forwards permissions: inherit if applicable.

Step 2 — Configure the publish workflow

actions/setup-node must set registry-url so npm’s CLI writes an .npmrc pointing at the public registry — provenance cannot be generated against a private, unconfigured registry:

- uses: actions/setup-node@v4
  with:
    node-version: 20
    registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build

Expected result: ~/.npmrc inside the runner contains an always-auth entry scoped to the registry, and npm run build has produced the dist/ artifacts the tarball will contain.

Step 3 — Publish with provenance

npm publish --provenance --access public

Expected output:

npm notice
npm notice 📦  @acme/[email protected]
npm notice === Tarball Contents ===
npm notice 12.4kB dist/index.mjs
npm notice 9.8kB  dist/index.cjs
npm notice 3.1kB  dist/index.d.ts
npm notice === Provenance Statement ===
npm notice sigstore verification successful
npm notice Signed provenance statement:
npm notice   predicateType: https://slsa.dev/provenance/v1
npm notice   subject: pkg:npm/%40acme/[email protected]
npm notice + @acme/[email protected]

HAZARD PREVENTION

Symptom: Publish succeeds, but the package page on npmjs.com does not show a “Provenance” badge.

Root cause: The --provenance flag was present but the OIDC exchange silently failed and npm fell back to a plain publish in an older CLI version, or the package was published from a fork whose GitHub identity does not match the source repository.

Fix: Pin npm CLI to 9.5+ (bundled with Node.js 18.17+/20+), and always publish from the canonical repository — provenance for forks is intentionally rejected since the Fulcio certificate encodes the actual repository identity.

Step 4 — Inspect the transparency log entry

Every provenance attestation is recorded in Sigstore’s Rekor log, independent of npm’s own infrastructure. Confirm the entry exists and matches your release:

npm view @acme/[email protected] dist.attestations.url

Expected output: a URL pointing to the attestation bundle, which references a Rekor log index that can be independently queried against https://search.sigstore.dev. A missing or empty field means the publish did not attach an attestation — treat that as equivalent to a failed provenance publish, even if npm publish reported success.


Tooling Validation

Consumers — and your own CI, when installing dependencies — verify provenance and registry signatures with a single built-in command:

npm audit signatures

Sample pass output:

audited 148 packages
148 packages have verified registry signatures

12 packages have verified provenance attestations

Sample failure output when an attestation cannot be validated:

1 package has an invalid or missing provenance attestation

  @acme/[email protected] - could not verify attestation against Rekor log

Wire this into CI as a hard gate before deployment:

- run: npm ci
- run: npm audit signatures

A nonzero exit code from npm audit signatures fails the job, preventing a deploy from proceeding on unverifiable dependencies. See verifying package provenance with npm audit signatures for the full verification workflow and how to interpret partial-coverage results.


Compatibility Matrix

Environment --provenance flag OIDC token support Transparency log lookup Notes
npm 9.5–9.x Yes GitHub Actions only Yes First release supporting provenance
npm 10.x Yes GitHub Actions, GitLab CI Yes Adds GitLab CI OIDC support
npm 11.x Yes GitHub Actions, GitLab CI, others Yes Adds OIDC trusted publishing (token-free)
Node.js 18.17+ Bundled npm supports it Minimum for provenance without manual npm upgrade
Node.js 20+ / 22+ Bundled npm supports it Recommended for new pipelines
GitHub Actions Yes Native id-token: write Yes Most common provenance environment
GitLab CI Yes Native id_tokens Yes Requires CI_JOB_JWT-successor OIDC config
Local machine / unsupported CI No No N/A No OIDC issuer available; flag has no effect

What the Attestation Actually Contains

When npm publish --provenance succeeds, npm stores two signed documents alongside the tarball, and understanding what is inside them explains both what provenance proves and what it deliberately leaves out. The first is a provenance attestation: an in-toto statement whose subject is the SHA-512 digest of the published tarball and whose predicate is a SLSA build definition recording the source repository, the exact commit SHA, the workflow file path, and the runner environment that produced it. The second is a publish attestation, which records that the npm registry itself accepted that tarball for that package name at that moment.

Anatomy of a provenance attestation The attestation bundle contains a subject block naming the tarball digest, a predicate block recording the source repository, commit and workflow, a short-lived Fulcio signing certificate bound to the workflow identity, and an inclusion proof from the Rekor transparency log. Inside the attestation bundle subject name: pkg:npm/@scope/[email protected] digest.sha512: 3f9c1a… binds the claim to exact bytes predicate (SLSA build) repository + commit SHA workflow path + run id builder: GitHub-hosted runner Fulcio certificate short-lived, ~10 minute validity identity = the workflow, not a user issued against the OIDC token Rekor inclusion proof append-only transparency log signed entry timestamp makes silent re-signing detectable

The short-lived certificate is the part that surprises most authors: there is no long-lived signing key to store, rotate, or leak. Sigstore’s Fulcio issues a certificate valid for roughly ten minutes, bound to the workflow identity (repo:org/name:ref:refs/tags/v1.4.0) presented in the runner’s OIDC token, and that certificate is thrown away as soon as the signature is made. Verification does not need the key — it needs the transparency log entry, which proves the signature existed while the certificate was valid.

You can read all of this back from the registry without installing anything:

npm view @scope/[email protected] --json | jq '.dist.attestations'
{
  "url": "https://registry.npmjs.org/-/npm/v1/attestations/@scope%[email protected]",
  "provenance": { "predicateType": "https://slsa.dev/provenance/v1" }
}

Fetching that URL returns the full bundle, including the buildDefinition.externalParameters.workflow block naming the exact .github/workflows/release.yml path and the resolvedDependencies entry pinning the commit. A consumer comparing that commit against the repository’s tag history can confirm the tarball was built from public source — which is the entire point.

Trust Boundaries: What Provenance Does Not Prove

Provenance answers one question well — did these bytes come from that commit, built by that workflow? — and it is easy to over-read it as a general safety guarantee. It is not one. The attestation says nothing about whether the source code is benign, whether the dependencies it bundled are trustworthy, or whether the maintainer who merged the commit intended a release at all.

What provenance covers and what it does not A left-to-right chain from source authorship through code review, dependency resolution, the build, and publication. Provenance covers only the build-to-publish span; authorship, review quality, and dependency trust sit outside the attested boundary. The attested span is narrower than it looks not attested who wrote the code how carefully it was reviewed whether a maintainer account was compromised attested by provenance this commit produced these bytes on this workflow, on this runner at this logged point in time with a workflow identity, not a token not attested bundled dependency safety postinstall script behaviour whether the release was intended at all Provenance shifts the question from "do I trust this publisher?" to "do I trust this repository?"

That shift is still worth a great deal. Before provenance, a package’s only link to its source was a repository field in package.json that anyone could write — a compromised maintainer token could publish a tarball built from entirely different code and nothing about the registry metadata would betray it. With provenance, that attack requires compromising the repository or the workflow, both of which leave a public trail: a commit in history, a workflow-file change in a diff, a Rekor entry with a timestamp.

Three limits are worth stating plainly to consumers who ask:

  • A verified attestation is not a code review. npm audit signatures confirms the linkage, not the intent. Malicious code committed to a public repository and built through a legitimate workflow produces a perfectly valid attestation.
  • Bundled dependencies inherit no guarantee. If your build inlines a transitive package, the attestation covers your build step; it says nothing about the provenance of what you inlined. Keeping dependencies external, as covered in the tree-shaking guidance on reducing dependency weight, keeps the attested surface honest.
  • Attestations expire from caches, not from truth. Registry mirrors and offline proxies may not carry the attestation bundle at all, so a consumer behind a mirror can get the tarball without the ability to verify it. That is a mirror configuration problem, and it is worth flagging in your install documentation.

HAZARD PREVENTION

Symptom: npm audit signatures reports 1 package has a missing or invalid signature for your freshly published version, even though the release workflow logged a successful provenance upload.

Root cause: The package was republished — or the version was re-tagged — through a path that did not carry --provenance, so the registry now holds a tarball whose digest does not match the earlier attested subject.

Fix: Never publish a version through two different paths. Make --provenance unconditional in the single reusable publish job, and gate local publishing entirely ("prepublishOnly": "node -e \"if(!process.env.CI) throw new Error('publish from CI only')\"").


Enforcing Provenance on the Consuming Side

Publishing attestations only pays off when somebody checks them, and the checking side has its own set of practical constraints. npm audit signatures verifies every installed package that carries a registry signature and reports the subset that also carries provenance, so the natural place to run it is immediately after npm ci in a consumer’s own pipeline:

npm ci
npm audit signatures
audited 412 packages in 3s

412 packages have verified registry signatures
118 packages have verified attestations

The gap between those two numbers is the interesting part. Registry signatures are applied by npm to everything; attestations exist only for packages whose maintainers opted into provenance. A consumer cannot demand provenance from the whole tree yet — the ecosystem coverage is far from complete — so a realistic policy targets a named allowlist of critical dependencies rather than a blanket requirement.

A workable enforcement script checks the packages you actually care about and fails the build if any of them lost their attestation between releases:

#!/usr/bin/env bash
set -euo pipefail

CRITICAL=("@scope/my-library" "@scope/auth-core" "@scope/crypto-helpers")

for pkg in "${CRITICAL[@]}"; do
  version=$(node -p "require('./node_modules/${pkg}/package.json').version")
  has=$(npm view "${pkg}@${version}" --json | jq -r '.dist.attestations.url // empty')
  if [ -z "$has" ]; then
    echo "FAIL: ${pkg}@${version} was published without provenance"
    exit 1
  fi
  echo "OK:   ${pkg}@${version} carries a provenance attestation"
done

Running that on every install turns a silent supply-chain regression into a build failure. If a dependency you rely on suddenly ships a version without provenance, that is worth a conversation before the version reaches production — it may be an innocent workflow change, or it may be a publish that bypassed CI entirely.

There is one more consumer-side detail that trips people up: lockfile integrity and provenance are different mechanisms. The integrity hash in package-lock.json guarantees that the bytes you install today are the bytes you installed last week; it says nothing about where those bytes came from. Provenance answers the origin question but is not consulted during a normal npm install. The two are complementary, and a security policy that mentions only one of them has a gap.

Finally, if you publish a package that others verify, treat the attestation as part of your public interface. Renaming the workflow file, moving the publish job to a different repository, or switching from GitHub-hosted runners to self-hosted ones all change the recorded build definition. None of those changes break installs — but a consumer with a strict policy pinned to workflow: .github/workflows/release.yml will see their check fail on your next release. Announce build-infrastructure changes in the changelog the same way you would announce a breaking API change, and keep the workflow path stable across releases even when the job’s internals evolve.

Organisations that want a stronger guarantee than “the attestation exists” can pin the expected build identity and diff it on every upgrade. Storing the expected repository, workflow path, and builder ID for each critical dependency in a small JSON policy file, then comparing the fetched attestation against it, catches the case that matters most: a dependency that still ships provenance, but whose provenance now points at a fork, a renamed workflow, or a self-hosted runner nobody approved. That check costs one registry request per package and turns an invisible infrastructure change into a reviewable diff in a pull request, which is exactly where a supply-chain decision belongs.


Guides in This Section



Back to CI/CD, Publishing & npm Provenance