In short

Publish from one CI workflow that the registry trusts through OIDC instead of a stored token. On npm, configure the trusted publisher (npm CLI 11.5.1 or later) and then set publishing access to require two-factor authentication and disallow tokens, because OIDC alone does not revoke old tokens. On PyPI, publish with pypa/gh-action-pypi-publish, which also uploads attestations. Keep publishing in its own job and verify downloads with npm audit signatures or pypi-attestations.

  • A trusted publisher binds publishing to a repository, workflow file and optional environment, and the registry issues a short-lived credential per run.
  • npm accepts GitHub Actions on GitHub-hosted runners, GitLab CI/CD on GitLab.com and CircleCI cloud; self-hosted runners are not supported.
  • npm revoked all classic tokens on 2025-12-09; granular write tokens default to 7 days and cannot exceed 90.
  • Configuring OIDC does not disable token publishing; select Require two-factor authentication and disallow tokens.
  • Since npm CLI 11.15.0, npm stage publish queues a release until a maintainer approves it with two-factor authentication.
  • Authentic update metadata can still be stale or rolled back: clients must check expiry, version order and target digests.

Verify it

  1. Run npm trust list acorn-cli and confirm a single GitHub Actions publisher with the expected repository, workflow file and environment.
  2. In the npm package settings, confirm Publishing access is set to require two-factor authentication and disallow tokens, then revoke unused tokens shown by npm token list.
  3. Publish a prerelease from the workflow and confirm the npm package page shows provenance linking the source commit and workflow run.
  4. In a consumer project, run npm audit signatures and expect verified registry signatures and attestations for the installed tree.
  5. Run pypi-attestations verify pypi --repository https://github.com/acorn/cli pypi:<file> and expect OK; a different repository must fail.
  6. Replay the update tests: a lower snapshot version, an expired timestamp and altered target bytes must each be rejected.
Applies to
  • npm CLI 11.5.1 or later for trusted publishing; 11.15.0 or later for npm stage and npm trust
  • pypa/gh-action-pypi-publish v1.14.2; attestations are on by default with trusted publishing

Package publication and update channels. Repository, publisher and client-update trust are distinct boundaries.

Learning goals and prerequisitesAbout 31 minutes: 8 to read and 23 for the exercise

What you will learn

  • Explain why trusted publishing does not remove parallel tokens automatically.
  • Bind registry authorization to a specific producer and artifact.
  • Evaluate freshness, rollback and target integrity separately.

Before you start

Package maintainers who publish to npm or PyPI and engineers who run software update channels

  • Understand artifact identity, signing trust and short-lived publication credentials.

Refresh first: Verify artifact signatures against an explicit trust policy

Go to the guided practice

Trusted publishing: from an OIDC token to a verified install

GitHub ActionsRegistryMaintainerPublish jobGitHub OIDCissuernpm registryConsumerTrusted publisher: repo, workflow, environment1Request a token(id-token: write)2Signed token:repo, workflow,ref, environment2OIDC token for a publish credential3Check claimsagainst thepublisher3Short-lived credential for thispackage3Tarball and provenance attestation4Approve a staged version with 2FA4Package,signature andprovenance5npm auditsignatures5Trust boundary1Step, described below
  1. Register the publisherA maintainer names the repository, workflow file and environment on npmjs.com or with npm trust github, then sets publishing access to require 2FA and disallow tokens.
  2. Request an identityThe publish job, the only job with id-token: write, asks GitHub's OIDC issuer for a signed token whose claims name the repository, workflow, ref and environment.
  3. Exchange itnpm publish sends that token to the registry, which accepts it only if the claims match the trusted publisher and returns a short-lived credential for this package.
  4. Publish with provenanceThe CLI uploads the tarball and, for a public package from a public GitHub or GitLab repository, a provenance attestation it generates automatically. A staged version waits until a maintainer approves it with 2FA.
  5. VerifyConsumers run npm audit signatures to check the registry signature and the provenance of what they installed. It proves where the package came from, not that its code is safe.
The OIDC token is a credential while the job runs: anything that executes in the publish job can request it, so that job installs and runs nothing it does not need.

Authorize a workflow and close alternative publication paths

Trusted publishing exchanges a CI workload identity for short-lived registry authority. It reduces reliance on a stored long-lived publish token, but the identity relationship still needs exact scope: package, repository, workflow and, where supported, protected environment. The registry must recognize the intended producer, and that producer must prevent unreviewed code from gaining its publishing authority. Possessing an OIDC token is not itself permission to publish any package. It is the control that stops account takeovers such as ua-parser-js's in 2021 (/domains/supply-chain/dependencies/case-study-event-stream/), provided the interactive publishing path also requires 2FA.

npm documents trusted publishing for GitHub Actions on GitHub-hosted runners, GitLab CI/CD on GitLab.com shared runners and CircleCI cloud, with npm CLI 11.5.1 or later and Node 22.14.0 or higher; Node 24.5.0 and later bundle a compatible npm, while Node 22 still ships npm 10 and needs an upgrade. Provenance is generated automatically for public packages published from public GitHub or GitLab repositories, but not from CircleCI or private repositories. Token publishing keeps working beside trusted publishing until you select Require two-factor authentication and disallow tokens, so configuring OIDC alone does not revoke an old token; interactive publishing with 2FA and the trusted publisher keep working after that change. PyPI documents trusted publishers for GitHub Actions, GitLab CI/CD on GitLab.com, Google Cloud and ActiveState, and mints a project-scoped token valid for 15 minutes. Supported providers and fields are registry-specific; do not copy a PyPI rule into npm and assume equivalent enforcement.

Publish to npm from GitHub Actions without a token

Register the publisher on npmjs.com or with npm trust github acorn-cli --file npm-publish.yml --repo acorn/cli --env release --allow-publish (npm 11.15.0 or later). The fields are case-sensitive, the workflow is named by its filename including .yml, and repository.url in package.json must match the repository exactly; npm does not check the configuration when you save it, so a mismatch first shows up as an "Unable to authenticate" (ENEEDAUTH) error at publish time. In the workflow below, tests and dependency installation run in a job without id-token: write. The publish job installs nothing, skips lifecycle scripts, and holds the only OIDC permission, so third-party code never runs while a publishing credential can be requested. At TanStack in May 2026, binaries restored from a poisoned cache read the OIDC token from a release job's memory (/domains/supply-chain/dependencies/case-study-shai-hulud/). npm also refuses trusted-publishing tokens from pull_request_target and issue_comment runs. package-manager-cache: false keeps a poisoned cache out of the release job, and no --provenance flag is needed.

npm CLI 11.15.0 also added staged publishing. npm stage publish uploads the tarball to a queue, and a maintainer approves it with npm stage approve or on npmjs.com after a two-factor challenge. A trusted publisher can be limited to staging with --allow-stage-publish. Configurations created since 2026-09-03 always allow staging and make direct publishing opt-in, a package can have up to 10 of them, and since 2026-10-02 one that has not published within 48 hours expires. Since 2026-09-18 granular tokens can be stage-only; npm targets January 2027 for ending direct publishing with tokens that bypass two-factor authentication.

Configuration.github/workflows/npm-publish.ymlYAML
name: Publish to npm
on:
  push:
    tags: ['v*']
permissions: {}
jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read # check out the tagged commit
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: '24'
          package-manager-cache: false
      - run: npm ci
      - run: npm test
  publish:
    needs: test
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read # check out the tagged commit
      id-token: write # exchange the OIDC token for a short-lived npm credential
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: '24'
          registry-url: 'https://registry.npmjs.org'
          package-manager-cache: false
      - run: npm publish --ignore-scripts

Publish to PyPI with attestations

PyPA recommends building in one job and publishing in another so that code injected into the build cannot use the publishing identity. The publish job below has only id-token: write, runs in an environment named pypi that can require reviewers, and uploads with pypa/gh-action-pypi-publish. With trusted publishing the action also signs a PEP 740 attestation for every distribution file, tied to the same workflow identity; set attestations: false only for a documented reason. Trusted publishing cannot be used from inside a reusable workflow, so call the action from the top-level workflow.

Configuration.github/workflows/pypi-publish.ymlYAML
name: Publish to PyPI
on:
  push:
    tags: ['v*']
permissions: {}
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read # check out the tagged commit
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: '3.14'
      - run: python -m pip install build==1.6.1 && python -m build
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: dist
          path: dist/
  publish:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/acorn-cli
    permissions:
      id-token: write # mint the 15-minute PyPI token and sign attestations
    steps:
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: dist
          path: dist/
      - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

Close the legacy publication paths

Moving to OIDC removes a secret from CI but leaves every older route open until you close it. Work through these checks for each package and organization.

  • npm: set Publishing access to Require two-factor authentication and disallow tokens once the trusted publisher works.
  • npm: list tokens with npm token list and revoke write tokens that no workflow needs. Classic tokens were revoked on 2025-12-09; new granular write tokens expire after 7 days by default and 90 days at most.
  • npm: prefer stage-only automation tokens and require a two-factor approval for each staged release.
  • Both registries: limit who can change trusted publisher settings, package owners and organization membership, and require phishing-resistant two-factor authentication for them.
  • GitHub: protect the release environment with required reviewers and deployment branch or tag rules, and protect the workflow file with CODEOWNERS review.
  • PyPI: delete API tokens that predate trusted publishing, and use one environment per index so TestPyPI and PyPI cannot share an identity by accident.

Bind publication authority to the bytes being released

A valid workload identity tells the registry which workflow is requesting publication. It does not ensure that the workflow packages the reviewed source or that a release artifact was not substituted. A clean protected build should create the package, compute its digest, produce release evidence and publish that same object. Compare the registry's resulting bytes or authoritative content identity against the approved package where the ecosystem supports it. Protect the branch and tags the publishing workflow builds from as well: /domains/supply-chain/ci-cd/source-repository-protection/.

Record package name/version, archive digest, source commit, workflow revision, run identity, registry destination and publication event. A tag named latest or a distribution channel is a mutable pointer, so consumers who require a particular release need a protected way to bind version and content. Restrict who can change publisher configuration and registry owners; otherwise an attacker may authorize a new producer without touching the original workflow. Keep recovery credentials separately governed, because removing all alternatives without a tested recovery path can strand legitimate releases.

Verify what consumers download

Consumers can check registry evidence without trusting the publisher web page. npm audit signatures verifies the registry signatures of every installed package and the provenance attestations of those that have them; it needs an installed tree, so install with --ignore-scripts first. For PyPI, pypi-attestations verify pypi downloads a file and its attestation and checks that the expected repository published it. Both checks prove where a package came from, not that its code is safe. For GitHub release assets, gh release verify and gh release verify-asset check the attestations of immutable releases.

Testverify-downloads.shShell
# npm: verify registry signatures and provenance attestations for the installed tree
npm ci --ignore-scripts
npm audit signatures

# PyPI: verify that a release file was published by the expected repository
python3 -m pip install pypi-attestations==0.0.30
pypi-attestations verify pypi --repository https://github.com/acorn/cli \
  pypi:acorn_cli-1.4.0-py3-none-any.whl

Protect freshness and authorized target selection

Software updates require more than an authentic old package. An attacker may replay an older signed release, freeze metadata or direct clients to an unauthorized target. The Update Framework separates metadata roles and uses signatures, versions and expiration to address update-system threats. Its verification procedure is broader than a single version comparison; applications should use a maintained implementation, such as python-tuf, go-tuf or tough, and specify their rollback and clock assumptions.

The simplified update policy below requires authenticated metadata, an unexpired timestamp, a non-decreasing snapshot version and a target digest matching authorized metadata. It is an exercise policy, not a replacement for TUF. An expired but correctly signed snapshot fails freshness. A higher package version with the wrong digest fails integrity. A legitimate emergency downgrade needs an explicit recovery/rollback design; disabling all freshness and rollback checks globally would authorize unrelated replay as well. Update metadata is therefore part of the product security boundary, with its own keys, rotation and incident response.

Signing proves where an update came from, not that it is safe to install everywhere at once. Ship updates, including rules and configuration that clients interpret, through rings with automatic halts and a tested rollback: /domains/supply-chain/releases/staged-rollout-rollback/.

Withdraw trust without losing the incident history

Revoke both the compromised publishing route and any parallel credentials. Inspect releases made during the exposure window, invalidate or deprecate affected versions through the registry's supported process, and communicate accurate affected identities through authorized incident channels. Deleting a tag cannot recall copies already installed. Clients need a trusted recovery update path, and organizations need evidence linking each remedial release to clean production.

Test rotation with synthetic identities and a staging registry: the old route must fail while the intended new route succeeds. For update clients, test stale metadata, rollback, altered target bytes and the valid next release. Retain failure reasons so an outage can be distinguished from a real rejection. Publishing and update availability matter, but an undocumented fallback to a stale token or expired metadata undermines the boundary you are trying to restore. The fixture below requires only comparing supplied claims and times, not obtaining credentials or publishing a package.

Guided practice

Acorn has moved to trusted publishing and is reviewing its remaining token plus three update candidates.

Supplied scenario material
Plain text
Publisher rule PUB3: package acorn-cli; repository acorn/cli; workflow release.yml; environment release. All four fields must match.
P1: all expected fields; registry accepts workload identity.
P2: repository acorn/cli, workflow debug.yml, environment release, package acorn-cli; workload identity authentic.
Legacy token T: still valid; can publish acorn-cli; stored in old CI system.
Update client: trusted metadata keys; highest snapshot version seen 12; current time 2026-10-07T12:00Z.
U1: authenticated, snapshot 13, expires 2026-10-08T12:00Z, target digest H13, downloaded H13.
U2: authenticated, snapshot 11, expires 2026-10-08T12:00Z, target H11, downloaded H11.
U3: authenticated, snapshot 13, expires 2026-10-07T11:59Z, target H13, downloaded H13.
Exercise update rule: reject unauthenticated, expired, lower snapshot, or target mismatch. Fictional owner: registry administrator Kit.
  1. 1. Apply publisher authorization

    Decide P1/P2 and explain what authentic workload identity does not imply.

    Expected result and reasoning

    P1 matches PUB3. P2 fails the workflow field even with authentic identity; a debug job is not an authorized release producer.

  2. 2. Find the incomplete migration

    Determine whether PUB3 removes the old publication risk.

    Expected result and reasoning

    No. Token T remains a separate publishing route. Kit must revoke/remove it after verifying the intended replacement and recovery process; OIDC configuration does not imply token revocation.

  3. 3. Evaluate update metadata

    Give the exact decision for U1/U2/U3 at the supplied time.

    Expected result and reasoning

    U1 passes the simplified rule. U2 fails rollback because 11 < 12. U3 fails expiration because 11:59Z is before 12:00Z. Valid signatures do not override either failure.

  4. 4. Add a target-substitution case

    Change downloaded digest for U1 to H99 and decide it.

    Expected result and reasoning

    Reject target mismatch: metadata authorizes H13 while downloaded bytes identify H99. A valid snapshot signature does not authenticate arbitrary replacement bytes.

  5. 5. Write the recovery tests

    Specify two staging tests and expected outcomes.

    Expected result and reasoning

    After rotation, a publish using T must be rejected and P1 must succeed for the staging package under its own scoped rule. The update client must retain its highest trusted state and reject U2/U3 while allowing valid U1. These are test criteria, not completed publication results.

Your deliverable: A publisher migration gap record and four update decisions covering authorization, rollback, expiry and bytes.

This is an abstract policy exercise. It is not a deployable registry configuration or full TUF verifier, and no package is published.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. npm trusted publishing
  2. PyPI trusted publishers
  3. The Update Framework overview
  4. Sigstore: verifying signatures and attestations
  5. GitHub changelog: npm authentication and token changes (2025-09-29)
  6. GitHub changelog: npm classic tokens revoked (2025-12-09)
  7. GitHub changelog: staged publishing and install-time controls for npm (2026-05-22)
  8. GitHub changelog: stage-only npm tokens (2026-09-18)
  9. GitHub changelog: multiple trusted publishing configurations for npm (2026-09-03)
  10. GitHub changelog: unvalidated npm trusted publishing configurations now expire (2026-10-02)
  11. npm Docs: npm trust. npm v11.
  12. npm Docs: npm audit signatures. npm v11.
  13. pypa/gh-action-pypi-publish
  14. PyPI Docs: producing attestations
  15. actions/setup-node: publishing to npm with trusted publishers
  16. The Update Framework specification
  17. CISA, NSA and ODNI (Enduring Security Framework): Securing the Software Supply Chain, Recommended Practices Guide for Developers, section 2.5 Deliver Code. August 2022.

Reference information

SDLC phase
Release
CAPEC attack patterns

Download this guide as Markdown