npm provenance attestations, introduced in 2023, let you publish a package that carries a cryptographic statement describing exactly which source commit and which build workflow produced it. Consumers can verify the attestation before installing, and supply chain scanners can flag installs that lack one. The publisher side takes about 20 minutes once per project; the consumer side is even faster. This tutorial walks through configuring a GitHub Actions workflow to publish a package with provenance, writing the matching package.json fields, verifying the attestation by hand, and blocking unsigned dependencies in your own CI.
Prerequisites: npm 10.5.0+, Node 20+, a GitHub repository with Actions, an npm registry token with automation privileges, and your package already published at least once manually. Time to complete: About 30 minutes.
What does provenance actually prove?
An npm provenance attestation is a sigstore bundle that binds the published tarball's digest to three claims: the GitHub repository URL, the git commit SHA, and the exact GitHub Actions workflow file and job that ran the publish. It does not prove the code is safe. It proves the tarball came from the specific CI run it claims to, which means an attacker who only compromises npm cannot ship a malicious tarball under your package name without also compromising your build pipeline. That is a significant elevation of attack cost.
The attestation is visible on the npm package page and at https://registry.npmjs.org/-/npm/v1/attestations/<name>@<version>, and can be verified with npm audit signatures or the cosign CLI.
How do I configure the GitHub Actions workflow?
Provenance requires the id-token: write permission on the job, a trusted publisher configuration on npm (or classic token with a 2FA bypass for CI), and the --provenance flag on npm publish. Create .github/workflows/publish.yml:
name: Publish
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
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 --if-present
- run: npm test --if-present
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The id-token: write permission is the key. It lets the workflow request an OIDC token from GitHub, which sigstore uses to issue a short-lived Fulcio certificate bound to the workflow identity. Without that permission, npm publish --provenance fails with "missing OIDC token".
What do I need in package.json?
npm requires a repository field that points to the public Git URL and a version that exactly matches the tag. A minimal package.json:
{
"name": "@acme/widget",
"version": "2.0.0",
"description": "Acme widget",
"repository": {
"type": "git",
"url": "git+https://github.com/acme/widget.git"
},
"publishConfig": {
"access": "public",
"provenance": true
}
}
The publishConfig.provenance: true is equivalent to passing --provenance on the command line. Setting it in package.json means someone who accidentally runs npm publish locally without the flag will either get an attestation (if they have OIDC set up) or a clear error, rather than silently publishing an unsigned tarball.
How do I verify the published attestation?
After the workflow runs and the package is published, verify the attestation from any machine:
npm view @acme/widget@2.0.0
npm audit signatures
Expected output:
audited 1 package in 2s
1 package has a verified registry signature
For a deeper check, pull the attestation directly:
curl -s "https://registry.npmjs.org/-/npm/v1/attestations/@acme/widget@2.0.0" | \
jq '.attestations[0].bundle.dsseEnvelope.payload' -r | \
base64 -d | jq '.subject, .predicate.buildDefinition.externalParameters'
Expected fields in the decoded payload:
subject[0].nameispkg:npm/@acme/widget@2.0.0subject[0].digest.sha512matches the tarball digestpredicate.buildDefinition.externalParameters.workflowreferencesacme/widget, the workflow file path, and the tag
A manual verification with cosign:
cosign verify-blob-attestation \
--bundle provenance.sigstore.json \
--new-bundle-format \
--certificate-identity-regexp "https://github.com/acme/widget/.github/workflows/publish.yml@refs/tags/v.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
widget-2.0.0.tgz
A clean verification prints Verified OK. Any deviation (wrong repo, wrong workflow, tampered tarball) prints an explicit failure.
How do I block consumers from installing unsigned dependencies?
In your own projects that consume npm packages, enforce provenance on install with npm install --audit-signatures:
npm install --audit-signatures
Any installed package that has a registry signature but fails verification blocks the install. Packages without any signature at all are reported but do not block by default. To be stricter, use the npm audit signatures command with a custom script that fails on missing attestations:
#!/bin/bash
OUT=$(npm audit signatures --json)
UNSIGNED=$(echo "${OUT}" | jq '.unsigned | length')
INVALID=$(echo "${OUT}" | jq '.invalid | length')
if [ "${INVALID}" -gt 0 ] || [ "${UNSIGNED}" -gt 20 ]; then
echo "Signature audit failed: invalid=${INVALID} unsigned=${UNSIGNED}"
exit 1
fi
The threshold of 20 unsigned is a starting point; tighten it over time as more of the npm ecosystem adopts provenance. Hard-zero on unsigned is aspirational today but will become practical within a couple of years.
How do I test the publish flow before cutting a real release?
Use an npm test registry such as Verdaccio, or dry-run the publish without actually uploading:
npm publish --provenance --dry-run
The dry-run still exercises the attestation signing pipeline in the workflow (when run from Actions) but skips the final upload. The workflow log shows the attestation payload, which you can inspect the same way as a real publish. This is the best way to catch a missing id-token: write permission or a misconfigured repository URL before the first real release.
Pair the dry-run with a local linter that checks package.json for the required fields:
jq -e '.repository.url and (.publishConfig.provenance == true)' package.json \
> /dev/null || { echo "package.json missing provenance config"; exit 1; }
Add this to pre-commit so provenance misconfiguration is caught in the first PR that introduces it.
How Safeguard Helps
Safeguard tracks provenance attestations across every npm dependency your projects pull in, flagging packages that regressed from a signed to an unsigned release and packages whose attested source repo no longer matches the declared home page. Griffin AI cross-references provenance with reachability, so a missing attestation on a package you actually call gets escalated while a missing one on a build-time-only dependency is deprioritized. Policy gates block merges that introduce packages without valid attestations, and your own published packages appear in the Safeguard inventory with attestation status alongside SBOM and vulnerability data, giving consumers a single record to verify.