---
key: "trusted-publishing-updates"
title: "Protect package publication and the update channel"
url: "https://softwaresecurity.com/domains/supply-chain/releases/trusted-publishing-and-updates/"
api: "https://softwaresecurity.com/api/v1/articles/trusted-publishing-updates.json"
domain: "supply-chain"
topic: "Software supply chain"
type: "Guide"
identifiers: ["CWE-494","A03:2025","A08:2025","CAPEC-657"]
tags: ["Workload identity","Dependency management","Signing and software updates","JavaScript and Node.js","Python"]
stages: ["release"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Protect package publication and the update channel

Constrain release identities, verify what consumers receive, and prepare for publisher or update-key compromise.

Canonical URL: https://softwaresecurity.com/domains/supply-chain/releases/trusted-publishing-and-updates/

## 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.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| npm CLI for trusted publishing | 11.5.1 or later with Node 22.14.0 or later | Node 24.5.0 and later bundle a compatible npm 11; Node 22 still bundles npm 10. | npm trusted publishing |
| npm package publishing access | Require two-factor authentication and disallow tokens | — | npm trusted publishing |
| npm granular write tokens | 7-day default lifetime, 90-day maximum | — | GitHub changelog: npm authentication and token changes (2025-09-29) |
| npm login session | 2 hours, with two-factor authentication enforced for publishing | — | GitHub changelog: npm classic tokens revoked (2025-12-09) |
| PyPI trusted publishing token | Project-scoped, valid for 15 minutes | — | PyPI trusted publishers |
| GitHub Actions publish job permissions | `id-token: write` (and `contents: read` for npm) in the publish job only | — | pypa/gh-action-pypi-publish |

## 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.

## 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.

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

Before you start:

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

Time: About 31 minutes: 8 to read and 23 for the exercise

## 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.

```yaml
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.

```yaml
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.

```bash
# 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 material

```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. Apply publisher authorization

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

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

### 2. Find the incomplete migration

Determine whether PUB3 removes the old publication risk.

Worked answer: 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. Evaluate update metadata

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

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

### 4. Add a target-substitution case

Change downloaded digest for U1 to H99 and decide it.

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

### 5. Write the recovery tests

Specify two staging tests and expected outcomes.

Worked answer: 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.

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

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

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/)
- [PyPI trusted publishers](https://docs.pypi.org/trusted-publishers/)
- [The Update Framework overview](https://theupdateframework.io/docs/overview/)
- [Sigstore: verifying signatures and attestations](https://docs.sigstore.dev/cosign/verifying/verify/)
- [GitHub changelog: npm authentication and token changes (2025-09-29)](https://github.blog/changelog/2025-09-29-strengthening-npm-security-important-changes-to-authentication-and-token-management/)
- [GitHub changelog: npm classic tokens revoked (2025-12-09)](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/)
- [GitHub changelog: staged publishing and install-time controls for npm (2026-05-22)](https://github.blog/changelog/2026-05-22-staged-publishing-and-new-install-time-controls-for-npm/)
- [GitHub changelog: stage-only npm tokens (2026-09-18)](https://github.blog/changelog/2026-09-18-stage-only-npm-tokens-for-safer-automation/)
- [GitHub changelog: multiple trusted publishing configurations for npm (2026-09-03)](https://github.blog/changelog/2026-09-03-multiple-trusted-publishing-configurations-for-npm/)
- [GitHub changelog: unvalidated npm trusted publishing configurations now expire (2026-10-02)](https://github.blog/changelog/2026-10-02-unvalidated-npm-trusted-publishing-configurations-now-expire/)
- [npm Docs: npm trust](https://docs.npmjs.com/cli/v11/commands/npm-trust/). npm v11.
- [npm Docs: npm audit signatures](https://docs.npmjs.com/cli/v11/commands/npm-audit/). npm v11.
- [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish)
- [PyPI Docs: producing attestations](https://docs.pypi.org/attestations/producing-attestations/)
- [actions/setup-node: publishing to npm with trusted publishers](https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md)
- [The Update Framework specification](https://theupdateframework.github.io/specification/latest/)
- [CISA, NSA and ODNI (Enduring Security Framework): Securing the Software Supply Chain, Recommended Practices Guide for Developers, section 2.5 Deliver Code](https://www.cisa.gov/sites/default/files/publications/ESF_SECURING_THE_SOFTWARE_SUPPLY_CHAIN_DEVELOPERS.PDF). August 2022.