Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

Setting Up npm Provenance with GitHub Actions

npm provenance produces a signed, publicly verifiable attestation linking a published tarball to the exact source commit and CI workflow that built it. Consumers can confirm a package was built from the repository it claims, on a trusted CI runner, without taking the publisher's word for it. This page sets up provenance end to end from GitHub Actions: the OIDC permission, the publish flag, the sigstore attestation, and how to verify the result.

Why Provenance

A standard npm publish proves only that someone with a token uploaded a tarball. Provenance adds a cryptographically signed statement — anchored in the public sigstore transparency log — asserting this tarball was built from this commit by this workflow. That closes the gap between "the source on GitHub" and "the bytes on the registry," and it is a foundational building block for the broader controls in Supply-Chain Security Hardening.

Provenance attestation flow A GitHub Actions OIDC token is exchanged for a sigstore certificate that signs an attestation, which npm publishes alongside the tarball to the registry. OIDC token id-token: write sigstore signs attestation npm publish --provenance registry verified badge
GitHub's OIDC identity is exchanged for a short-lived sigstore certificate that signs the attestation npm publishes beside the tarball.

Prerequisites

  • The package is published to the public npm registry (provenance requires public publishing).
  • The repository is public, or you accept that provenance metadata references the repo.
  • You publish from GitHub Actions (the OIDC issuer npm trusts). The end-to-end publish mechanics are in npm Registry Publishing Workflows.
  • A recent npm CLI (provenance support landed in npm 9.5+; use Node 20's bundled npm or newer).
Prerequisites Prerequisites in production JavaScript package workflows. Prerequisites Prerequisites in production JavaScript package workflows.
Prerequisites — the core idea of this section at a glance.

Numbered CI Setup

  1. Grant the workflow an OIDC token. Provenance is signed using GitHub's OIDC identity, which requires the id-token: write permission at the job (or workflow) level. Without it the publish aborts at the attestation step.
  2. Pin the registry in setup-node so the auth line is generated and the registry is unambiguous.
  3. Build deterministically before publishing so the attested artifact matches the committed source.
  4. Pass --provenance (or set publishConfig.provenance: true) on the publish step.
  5. Provide the publish token through NODE_AUTH_TOKEN, never echoed.
Numbered CI Setup Pinning provenance in the manifest means a local or alternate publish path cannot silently drop it: Numbered CI Setup Pinning provenance in the manifest means a local or alternate publish path cannot silently drop it:
Numbered CI Setup — the core idea of this section at a glance.

Manifest opt-in (optional but recommended)

Pinning provenance in the manifest means a local or alternate publish path cannot silently drop it:

{
  "publishConfig": {
    "access": "public",
    "provenance": true
  }
}

Full workflow

# .github/workflows/publish-provenance.yml
name: Publish with provenance
on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  id-token: write          # mandatory: lets npm fetch an OIDC token for 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/'
          cache: 'npm'

      - name: Install
        run: npm ci

      - name: Build
        run: npm run build

      - name: Verify tarball contents
        run: npm pack --dry-run

      - name: Publish with provenance
        run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

The id-token: write permission and the GitHub-hosted runner together let npm exchange GitHub's OIDC token for a short-lived sigstore signing certificate. The CLI then records a signed attestation (build metadata, source commit, workflow path) in the public transparency log and uploads it alongside the tarball.

Validation

After the workflow runs, confirm the attestation exists and verifies:

Validation After the workflow runs, confirm the attestation exists and verifies: Validation After the workflow runs, confirm the attestation exists and verifies:
Validation — the core idea of this section at a glance.
# Show the published version's metadata (provenance is recorded in the registry)
npm view @acme/widget

# Verify attestations for the installed package against sigstore
npm audit signatures

npm audit signatures checks both the registry signature and any provenance attestations for installed packages and reports how many verified. On the registry web UI, a package published this way shows a "provenance" / verified-build indicator linking back to the exact workflow run and commit.

Confirm the attestation was created and is verifiable after publishing:

# The published version should list attestations
npm view your-pkg dist.attestations
# Verify signatures and provenance across the installed tree
npm audit signatures
# Inspect the provenance's claimed source and build
npm view your-pkg --json | jq '.dist.attestations'

A populated dist.attestations confirms provenance generated, and a clean npm audit signatures confirms it verifies against the claimed source. If dist.attestations is empty after a --provenance publish, the workflow lacked id-token: write or ran in an unsupported environment.

Guardrails

  • Keep id-token: write scoped to the publish job only — do not grant it workflow-wide if other jobs do not need it.
  • Set publishConfig.provenance: true so provenance is the default and cannot be dropped by an ad-hoc publish.
  • Publish only from tag-triggered workflows on protected branches so the attested commit is always a reviewed, tagged release.
  • Pin action versions (@v4) and the Node version so the build environment recorded in the attestation is reproducible.
  • Layer provenance with the build-integrity controls in Adding SLSA Provenance to Package Releases for full supply-chain coverage.
Guardrails Guardrails in production JavaScript package workflows. Guardrails Guardrails in production JavaScript package workflows.
Guardrails — the core idea of this section at a glance.

Verifying provenance as a consumer

Publishing provenance is only half of the trust chain; the other half is consumers verifying it, and doing so scales through automation rather than manual inspection. Running npm audit signatures walks the installed dependency tree and verifies registry signatures and provenance attestations, failing when a package that should carry provenance does not verify. Wiring it into CI turns provenance from a badge nobody checks into an enforced property: a dependency that loses its expected signature, or whose attestation does not match its claimed source, fails the build.

Consumer verification audit signatures verifies the tree against attestations. npm audit signatures walk the tree verify attestations source + integrity fail on mismatch block install
Verifying provenance in CI turns a badge into an enforced supply-chain property.

For security-critical dependencies specifically, prefer ones that publish provenance and treat a missing attestation as a reason to pin the version and review before adopting. What provenance proves is origin and integrity — that the tarball came from the claimed source and build, unmodified — not that the code is benign, so it is one layer of a supply-chain posture rather than a guarantee of safety. Combined with an audit threshold, host allow-listing, and blocked install scripts, verifying provenance closes the specific gap of a hijacked account or a tampered publish, which is a large and important class of attack that the other defenses do not address.

Common provenance setup failures

The setup usually fails for one of a few specific reasons, each with a clear fix. The most common is a missing id-token: write permission, without which the runner cannot obtain the OIDC token and --provenance errors; adding the permission at the job level resolves it. The second is publishing from an unsupported environment — provenance requires a CI provider npm trusts, so a local npm publish --provenance fails because there is no trusted identity to sign the attestation. The third is a mismatch between the package's repository field and the actual source repository, which makes the attestation bind to the wrong origin or fail validation.

Setup failure Why a provenance publish fails and the fix. Where does it fail? missing id-token add write perm local publish use CI provider wrong repository fix repository field
Align the identity, the permission, and the source binding.

A subtler failure is a private or restricted package where provenance is configured but the access level conflicts, or a monorepo where the workflow publishes several packages and the repository field's directory is not set per package. The fix in each case is to make the identity, the permission, and the source binding consistent: the workflow has id-token: write, runs on a supported provider, and each package declares its correct repository (with directory for a monorepo). With those aligned, the attestation generates cleanly and consumers can verify a precise link from the tarball back to the exact commit and workflow that produced it.

Provenance in a monorepo publishing many packages

Publishing provenance from a monorepo adds a wrinkle that a single-package repo does not have: each package's attestation must bind to the correct source location, which means the repository field needs a directory entry per package. Without it, the attestation for @acme/ui might claim the repository root rather than packages/ui, which either binds to the wrong source or fails validation. Setting the repository.directory for each package so the attestation points at the exact subdirectory is what makes per-package provenance in a monorepo correct.

Monorepo provenance Per-package repository.directory binds each attestation correctly. id-token: write once job level repository.directory per package publish --provenance per-package attestation
Each package needs its repository.directory so its attestation points at the right source subdirectory.

The workflow structure matters too. A release job that publishes several packages needs id-token: write once at the job level, and each npm publish --provenance invocation generates its own attestation for the package it publishes. Pairing this with a release tool that computes each package's version and publishes only the affected set means every published package carries a provenance attestation bound to its own source directory and the same trusted build. The result is that a consumer of any one package in the monorepo can verify a precise link from that package's tarball back to the exact commit and subdirectory it was built from, which is the same guarantee a single-package repo provides, extended correctly across the workspace.

Provenance, OIDC, and eliminating the publish token

Provenance and token-free publishing are two outcomes of the same OIDC setup, and configuring both removes the last long-lived secret from the release path. When a workflow has id-token: write, it can exchange its verified identity not only for the provenance attestation but, on registries that support it, for short-lived publish rights — so there is no standing NPM_TOKEN to leak, rotate, or protect. The identity that signs the attestation is the identity that authorizes the publish, which is why moving to OIDC simplifies the release even as it strengthens it.

Token-free release One OIDC identity signs provenance and authorizes publish. id-token: write verified identity signs provenance + publish rights no standing token softest target removed
OIDC produces both the attestation and short-lived publish rights — no standing token.

Where a registry still requires a token alongside provenance, the token should be a granular, package-scoped automation token read from a secret store, so its exposure is bounded and its rotation is a contained operation. But the direction of travel is toward OIDC for both authorization and provenance, which turns the release job into something with no secret to steal: a verified CI identity produces short-lived rights and a signed attestation, and the published artifact carries a cryptographic link back to the exact build. Setting this up once — id-token: write, --provenance, and OIDC publishing where available — is what makes a package's releases both verifiable by consumers and free of the standing credential that is otherwise the softest target in the supply chain.

Frequently Asked Questions

Why does my publish fail with a provenance error even though I added --provenance? The job almost certainly lacks the id-token: write permission. Provenance is signed with GitHub's OIDC identity, so without that permission npm cannot obtain the signing token and aborts before upload. Add permissions: id-token: write to the job.

Can I generate npm provenance from CI providers other than GitHub Actions? npm provenance currently trusts a specific set of OIDC issuers, with GitHub Actions and GitLab CI being the supported, documented paths. From an unsupported runner the attestation cannot be produced; publish from a supported provider instead.

Does provenance work for private or scoped-restricted packages? No. Provenance requires public publishing, because the attestation is recorded in a public transparency log. For a scoped package, ensure access is public; see Publishing Scoped Packages to npm.

How do consumers verify the provenance of a package they installed? They run npm audit signatures, which validates registry signatures and provenance attestations for installed packages against sigstore and reports how many verified. The registry web UI also surfaces a verified-build indicator linking to the originating commit and workflow run.

Why does npm publish --provenance fail locally?

Provenance requires a trusted CI provider that can issue an OIDC identity for the build; a laptop has no such authority, so there is nothing to sign the attestation. Publish from a supported CI environment (like GitHub Actions) with id-token: write permission.

What permission does provenance need in GitHub Actions?

id-token: write at the job level, which lets the runner obtain the OIDC token npm uses to generate the attestation. Without it, --provenance cannot create the signed link between the tarball and its source build.

How do consumers check my package's provenance?

They run npm audit signatures, which verifies registry signatures and provenance attestations across the installed tree, confirming your tarball came from its claimed source and build. Wiring it into their CI makes the check enforced rather than manual.

Is dist.attestations empty after I published with --provenance?

Then the workflow lacked id-token: write or ran in an unsupported environment, so no attestation was generated. Add the job-level permission, publish from a supported CI provider, and re-check npm view your-pkg dist.attestations.

Does provenance let me publish without an NPM_TOKEN?

On registries that support OIDC publishing, yes — the same id-token: write identity that signs the provenance attestation can be exchanged for short-lived publish rights, so there is no standing token to leak or rotate. Where a token is still required, use a granular, package-scoped one from a secret store.

Related

npm Registry Publishing Workflows