Skip to content
← Blog

Technical explainer

Trusted Publishing for npm and PyPI: OIDC-backed package releases

4 min read

Trusted Publishing is the name PyPI uses for OpenID Connect-based publishing without long-lived API tokens. npm has a closely related feature via npm provenance and the "Trusted Publishing" model adopted from PyPI. In both cases, the registry authenticates a CI workflow's identity directly and accepts a publish from that workflow without any pre-shared secret. This post walks through what it is, how to set it up, and why it materially reduces supply-chain risk.

The problem long-lived tokens cause

The traditional publishing flow is: maintainer generates a publish token from the registry, stores it in a CI secret, and the CI workflow uses it to publish on every release. The token has full publishing rights to the maintainer's packages.

Three things go wrong with this model:

  1. Tokens leak. Logged accidentally, committed to the wrong repo, exfiltrated by malicious dependencies, or extracted from a developer's machine.
  2. Tokens are broad. Default tokens can publish any package the user controls. A worm that finds the token can publish to all of them.
  3. Tokens persist. A token issued years ago is still valid until rotated, and many projects never rotate.

Trusted Publishing replaces the token with a short-lived OIDC-based identity assertion that the registry verifies directly.

How OIDC publishing works

The flow on each release looks like this.

  1. Your CI workflow runs (typically GitHub Actions, but other providers work too).
  2. The CI provider issues a JSON Web Token (JWT) signed with its OIDC keys, identifying the workflow: repo, branch or tag, workflow file, environment.
  3. The CI workflow sends this JWT to the registry's OIDC endpoint as part of the publish request.
  4. The registry verifies the JWT signature against the CI provider's public keys and checks that the workflow identity matches a Trusted Publisher registered for the package.
  5. If everything matches, the publish proceeds.

There is no long-lived token in this flow. The JWT is valid for minutes; the matching is per-package; the entire chain of trust is to the CI provider's identity infrastructure.

Setting it up on PyPI

PyPI's Trusted Publishing supports GitHub Actions, GitLab CI, ActiveState, and Google Cloud out of the box.

For GitHub Actions:

  1. On PyPI, navigate to your project's settings and add a Trusted Publisher entry. You provide: the GitHub repo, the workflow filename, and an environment name (recommended).
  2. In your workflow, request the OIDC token by adding id-token: write to permissions and use the pypa/gh-action-pypi-publish action.
permissions:
  id-token: write
jobs:
  publish:
    runs-on: ubuntu-latest
    environment: release
    steps:
      - uses: actions/checkout@v4
      - run: python -m build
      - uses: pypa/gh-action-pypi-publish@release/v1

No token. No secret in CI. PyPI authenticates the workflow directly.

Setting it up on npm

npm's equivalent uses npm provenance attached to the publish, again with OIDC. Trusted Publishing for npm has been progressively rolling out; the basic shape:

permissions:
  id-token: write
  contents: read
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 publish --provenance --access public

--provenance triggers the OIDC flow and produces a Sigstore attestation linking the published artifact to the workflow run.

What it changes in your threat model

Trusted Publishing removes the maintainer-machine and CI-secret-store as compromise vectors for publishing. Specifically:

Stolen developer-machine credentials cannot publish. There is no token on the developer's machine. The token used for publishing is created by the CI provider on each run and discarded.

Leaked CI logs do not leak a publishing token. The OIDC JWT is short-lived and includes a nonce; it cannot be replayed.

A worm scanning for ~/.npmrc finds nothing useful. Even if it finds an old token, ideally that token has been migrated away from. With Trusted Publishing, the config doesn't carry a publishing token at all.

Per-package and per-environment scoping is straightforward. A Trusted Publisher entry binds the workflow to a specific package; you can configure separate entries per package, with separate environments, separate approval requirements.

What it does not change

Trusted Publishing does not protect against compromise of the build environment itself. If the GitHub Actions workflow file is modified to inject malicious code, or if a malicious dependency runs at build time and modifies the artifact before publish, the publish is still authentic — it really did come from the workflow — but it carries a malicious payload.

Two practices reduce that risk:

  • Use protected branches and required reviews on the workflow file itself.
  • Pin the actions used in the workflow to specific commit SHAs (uses: actions/checkout@v4 is mutable; uses: actions/checkout@a1b2c3d4... is not).

Migration path

The migration is conservative. Set up the Trusted Publisher entry first, leave the existing token-based publish in place. Make a test release using the new flow on a feature branch or a pre-release tag. Verify it succeeds. Then migrate the production publishing workflow. Then revoke the long-lived publish token.

For projects that publish from a single workflow on a single repo, the migration takes 30 minutes. For organizations with many packages and complex publishing matrices, plan for a project — but the result is that no broad publishing token exists in your infrastructure at all.

Takeaway

Trusted Publishing is the highest-leverage supply-chain hardening any maintainer can do today. It removes the most-stolen credential class entirely. The work to enable it is small; the threat surface it eliminates is large. If you publish to npm or PyPI from CI, switch this month.