Skip to main content
INS // Insights

npm Trusted Publishing to Kill CI Tokens

Updated September 2026 · 8 min read
npm Trusted Publishing to Kill CI Tokens

npm trusted publishing is how a release pipeline stops being a long-lived granular access token in GitHub Actions secrets. Classic _authToken values are bearer secrets. They live in the repo, in forks that once had access, in logs when someone printed env, and in the laptop of whoever generated them. Trusted publishing lets npm accept an OIDC identity from the CI provider and issue a short-lived publish right for that workflow. npm documents the feature as trusted publishing with provenance (npm trusted publishing).

Rutagon treats this as a credential-elimination cutover, not a registry settings tour. We inventory tokens, pin the workflow, publish once with OIDC, then delete the classic token. We do not sell a vault. We do not claim Rutagon is SOC 2 certified because a token disappeared.

Why Classic Publish Tokens Fail the Review

A granular access token with publish on @your-scope/* is standing privilege. Rotation policies create a second token during overlap and never delete the first. Automation tokens marked "bypass 2FA" are worse. They are the service-account password of the JavaScript ecosystem.

CISA's secure-by-design guidance keeps returning to short-lived, phishing-resistant credentials (CISA Secure by Design). The same physics apply to package registries. A token that works from any IP until expiry is a laptop leak waiting for a scanner.

Supply-chain incidents in public reporting have repeatedly involved stolen publish credentials. You do not need a named breach in this article to justify deleting the class of secret. The control is architectural: the registry should only accept a publish that came from this workflow in this repo.

npm Trusted Publishing Cutover We Run

Wave 0 — Inventory. Every NPM_TOKEN, NODE_AUTH_TOKEN, and .npmrc in GitHub secrets, org secrets, and local release scripts. Rank by package privilege (entire org vs one package).

Wave 1 — Pin the publisher. On npm, connect the package or org to the GitHub (or GitLab) repository and the workflow file that is allowed to publish. The pin is the blast-radius control. A pin to *.yml is how a PR workflow becomes a publisher.

Wave 2 — Dual-run. One release uses OIDC. We watch the npm audit log for the trusted-publisher identity, not the old token name.

Wave 3 — Delete. Token revoked on npm, secret removed from GitHub, .npmrc lines that embed tokens gone. An alarm on secret recreation is optional and worth it.

# GitHub Actions — trusted publishing, no NPM_TOKEN
permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: npm-release
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          registry-url: "https://registry.npmjs.org"
      - run: npm publish --provenance --access public
        # identity comes from OIDC, not from secrets.NPM_TOKEN

The dangerous version is a reusable workflow in another repo that inherits id-token: write and is not the pin npm expects. We publish from one environment-protected job. Required reviewers on that GitHub Environment are the human gate. See GitHub environment protection with OIDC and workload identity federation migration. Capability: security automation.

Provenance Is a Bonus, Not the Control

--provenance attests that the package was built on GitHub Actions. That is useful for consumers and for your own SBOM story. It does not replace token elimination. A stolen classic token can still publish a package without provenance if you leave the token alive. Delete the token. Treat provenance as the receipt.

Private packages and org-restricted publishing follow the same pin. Internal registries that are not npmjs.com need their own OIDC story — GitHub Packages, CodeArtifact, or Artifactory. We do not pretend trusted publishing on npmjs.com cleans a second registry. Those tokens stay on the rotate long-lived AWS credentials style exception register until that registry federates.

Rollback Without Re-minting the Habit

If OIDC publish fails on a Friday release, the rollback is a time-boxed classic token with an expiry measured in hours, an owner, and a delete ticket already open. Teams that "put the token back until next quarter" have not migrated. We would rather delay one release than restore a standing secret as culture.

Monorepos with many packages should still use one publisher identity and a workspace-aware release tool. Twelve tokens for twelve packages is twelve copies. One trusted publisher with a tight workflow is the point.

What We Will Not Write

We will not publish a field guide for attacking the npm trust model. We will not turn this into a generic CI product comparison. We will show you the secret names in your org and the pin you are missing.

Org-level vs package-level pins matter. An org-wide trusted publisher is convenient and wide. A package-level pin is tighter when only release.yml in one repo should move @scope/ui. We start tight and expand if the monorepo release tool cannot live with it. Convenience is how a docs repo workflow becomes a publisher.

Provenance consumers — your own install CI — can fail closed if a package lacks provenance after the cutover. That is a feature. Do not disable the check to spare a forgotten package. Add that package to the pin list or stop publishing it from a laptop.

Self-hosted runners change the trust story the same way they do for AWS OIDC. The token is still OIDC, but who can enqueue the publish job is part of the control. We treat runner identity as in-scope. If any engineer can attach a runner and run release.yml, the npm pin is not enough.

We also search Actions logs and package.json publishConfig for leftover tokens after the cutover. A token in a leftover NPM_TOKEN org secret will get used the first time someone copies an old workflow. Delete the secret. If a human still publishes from a laptop, that is an exception with an owner and a hardware key — not the default path.

Talk to us about your credential elimination pilot →

907-841-8407 · contact@rutagon.com

Frequently Asked Questions

Does npm trusted publishing work with GitLab CI?

Trusted publishing is defined per CI provider npm supports. If your provider is on that list, we pin that identity. If it is not, we keep a scoped token on the exception register and pressure the registry or change the publisher job to a supported runner.

What about npm access tokens used only to install private packages?

Install tokens are a different class. Prefer project-scoped read tokens or a package-manager proxy with short-lived cloud identity. Do not reuse the publish token for npm ci.

Do we need a separate GitHub Environment for publish?

Yes if production packages can break customers. Environment reviewers plus the npm pin is defense in depth. The pin alone is necessary. The environment is how humans stay in the loop.

Will this break semantic-release or Changesets?

Those tools can publish with the environment's OIDC session if they do not insist on reading NPM_TOKEN. We adjust the release tool, we do not keep the token to spare the tool.

Is this a substitute for package signing?

No. Trusted publishing authenticates the publisher. Signing and provenance attest build. Do both if your threat model says so. Start by deleting the standing token.