JavaScript supply chain attacks are now one of the fastest ways to compromise production systems, and in 2026 the baseline for serious teams is no longer just npm audit. You need package provenance, signed build artifacts, policy checks, and repeatable CI verification before deployment. In this practical guide, you will implement a modern workflow using npm provenance, Sigstore Cosign, and SLSA-style build controls so you can detect tampering early and ship with confidence.
Why this matters in 2026
Most incidents today do not come from obvious vulnerabilities in your own code. They come from:
- Compromised maintainer accounts publishing malicious versions
- Dependency confusion between private and public package names
- Tampered build artifacts that do not match source
- Over-privileged CI tokens leaking to logs or third-party actions
The good news is the ecosystem has improved. npm supports provenance attestations, Sigstore gives keyless signing and transparency logs, and GitHub Actions OIDC can issue short-lived identity instead of static secrets.
Target architecture
We will implement a pipeline with these properties:
- CI builds your package from a pinned environment
- Build produces an artifact and SBOM
- Artifact is signed with Sigstore keyless flow
- Provenance is generated and stored
- Deploy stage verifies signatures and provenance before release
Prerequisites
- Node.js 22+
- npm 10+
- GitHub repository with Actions enabled
cosigninstalled locally (optional for local verification)
Step 1: Harden dependency resolution
Start by preventing accidental package substitution and lockfile drift.
# Fail CI if lockfile changes unexpectedly
npm ci
# Audit with production focus
npm audit --omit=dev
# Detect duplicate/transitive issues
npm ls --all
In .npmrc, force trusted registries and exact behavior:
registry=https://registry.npmjs.org/
strict-ssl=true
fund=false
audit=true
If you use private scope packages, explicitly map scopes:
@your-org:registry=https://npm.pkg.github.com
This closes a common dependency confusion hole where CI might resolve a public package instead of your private one.
Step 2: Generate SBOM during build
A software bill of materials (SBOM) gives you an inventory you can scan later, even if new CVEs appear after release.
# Install CycloneDX generator
npm i -D @cyclonedx/cyclonedx-npm
# Generate SBOM
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
Add this to your package.json scripts:
{
"scripts": {
"build": "tsc -p tsconfig.json",
"sbom": "cyclonedx-npm --output-file sbom.json"
}
}
Step 3: Enable npm provenance on publish
When publishing from GitHub Actions, npm can attach provenance metadata proving where and how the package was built.
Example workflow snippet:
name: publish
on:
push:
tags:
- 'v*'
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Important details:
id-token: writeis required for OIDC-based provenance- Use short-lived identity from OIDC, avoid long-lived cloud keys
- Pin Actions major versions and review third-party actions regularly
Step 4: Sign release artifacts with Sigstore Cosign
If you ship tarballs, Docker images, or compiled assets, sign them and verify before deployment.
# Build artifact
tar -czf dist.tgz dist/
# Keyless sign (uses OIDC identity)
cosign sign-blob dist.tgz --bundle dist.tgz.bundle
# Verify signature and certificate identity
cosign verify-blob dist.tgz \
--bundle dist.tgz.bundle \
--certificate-identity-regexp 'https://github.com/your-org/your-repo/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
This makes artifact tampering much harder because verification ties the artifact to your trusted CI identity and records it in transparency logs.
Step 5: Add policy checks before deploy
Security controls work only if they block unsafe releases. Add a deployment gate script:
#!/usr/bin/env bash
set -euo pipefail
ARTIFACT=dist.tgz
BUNDLE=dist.tgz.bundle
cosign verify-blob "$ARTIFACT" \
--bundle "$BUNDLE" \
--certificate-identity-regexp 'https://github.com/your-org/your-repo/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
# Minimal SLSA-style assertion example (pseudo-check)
if ! jq -e '.buildType and .builder.id and .invocation' provenance.json >/dev/null; then
echo "Provenance missing required fields"
exit 1
fi
echo "Verification passed"
Run this script as a mandatory pre-deploy job. If verification fails, deployment must stop automatically.
Production checklist
- Use
npm cionly in CI, nevernpm install - Commit and protect
package-lock.json - Require branch protection and signed commits for release branches
- Limit CI permissions to least privilege
- Rotate npm tokens and prefer automation tokens with narrow scope
- Store SBOM and provenance with each release artifact
- Block deploy when signature/provenance checks fail
Common pitfalls
1. Trusting only vulnerability scanners
Scanners find known CVEs, but they do not prove your artifact was built by trusted CI from expected source.
2. Overlooking transitive dependency risk
Your direct dependencies may look clean while deep transitive packages carry malware or typosquats.
3. Using wildcard action versions in CI
Always pin actions to trusted versions. Unpinned dependencies in CI are still supply chain risk.
Where to go next
After this baseline, add continuous dependency update automation, runtime allowlists for outbound traffic, and artifact verification in Kubernetes admission controllers. But even without those advanced layers, npm provenance plus Sigstore verification already closes major attack paths for most Node.js teams.
In 2026, the secure default is verifiable software delivery, not just vulnerability patching. Implement the steps above once, automate them in CI, and your releases become measurably safer without slowing down developers.

Leave a Reply