Open Source Security

PyPI Trusted Publishing: An Adoption Guide

Trusted Publishing replaces long-lived PyPI tokens with OIDC-issued short-lived credentials. A practical guide to adoption, pitfalls, and what it changes for your threat model.

Shadab Khan
Security Engineer
6 min read

When the Python Package Index turned on Trusted Publishing for all projects in April 2023 (PyPI blog, "Trusted Publishers for All Packages," 20 April 2023), it quietly became one of the most important supply-chain security changes any major registry has shipped. The idea is simple in one sentence and meaningful in all the others: instead of storing a long-lived API token in your CI system, your CI system presents a short-lived OIDC identity token to PyPI at publish time, and PyPI issues a minted credential scoped to that specific publish. As of mid-2024, more than 15,000 PyPI projects had adopted it, according to the PyPI status page and statistics dashboards maintained by Ee Durbin and Dustin Ingram. If you publish to PyPI from automation, this is the single largest security improvement available to you.

What Trusted Publishing Actually Does

Under the old model, publishing a package from GitHub Actions meant creating a PyPI API token, pasting it into a repository secret, and running twine upload. That token sat in CI storage indefinitely, could be exfiltrated by any workflow with access to it, and often had account-level scope — meaning a leaked token could push releases to every package the maintainer owned.

Trusted Publishing changes the flow entirely. At publish time, the CI job receives an OIDC ID token from its provider. The pypa/gh-action-pypi-publish action (or equivalent for other providers) presents that token to PyPI's token exchange endpoint. PyPI verifies the signature against the provider's published JWKS, checks that the issuer, repository, workflow, and environment claims match what the project has configured, and — if everything matches — returns a short-lived upload token valid only for that publish. The token lives in memory for the duration of the job and then vanishes.

The net result: no long-lived credential exists in the repository, no credential can be reused from a different workflow or branch, and compromise of a single CI run cannot produce a persistent foothold.

Supported Providers

As of August 2024, PyPI supports Trusted Publishing from:

GitHub Actions (first, launched April 2023), GitLab CI/CD (added September 2023), Google Cloud Workload Identity (added June 2023), and ActiveState (added early 2024). PEP 740, which describes the attestation side of the story, was accepted in 2024 and extends the model into signed publish attestations stored alongside each release.

If you publish from a provider that isn't on this list today — Jenkins on an internal runner, Azure DevOps, CircleCI — Trusted Publishing isn't yet an option and you'll need to stay with scoped API tokens for now. PyPI has been explicit that expanding provider support is an ongoing roadmap item.

Configuring Trusted Publishing on a Project

The configuration happens in two places: on PyPI and in your CI workflow.

On PyPI, navigate to the project's management page, open the "Publishing" tab, and add a new trusted publisher. You'll provide the owner, repository, workflow filename, and (optionally) the deployment environment name. PyPI stores these as the canonical trust record.

In your workflow, the change is typically a one-line update to the publish action. For GitHub Actions, a modern publish job looks like this: the workflow requests id-token: write permission, removes any password: field from the publish step, and relies on the action to perform the OIDC exchange. The action's README has tracked each iteration of this pattern; the current version as of 1.8.x is the reference.

The configuration review is where most adoption failures happen. Typos in the workflow filename, mismatches between the environment claim and the PyPI configuration, and branch protection gaps that let a fork trigger the workflow are all common. PyPI's error messages during a failed exchange are informative, and the "Publishing" tab shows the most recent failed attempts with enough detail to diagnose.

Where Environments Matter

The most valuable single knob in a Trusted Publishing configuration is the deployment environment. On GitHub Actions, an environment is a named protection boundary with its own required reviewers, wait timers, and secret scoping. Binding the PyPI trusted publisher to a specific environment means a pull request from a fork cannot publish — only a workflow running in the protected environment can, and that environment can require human approval.

The pattern I recommend for anything beyond personal projects: create a pypi or publish environment, require approval from at least one maintainer, and bind the PyPI publisher to it. This combines Trusted Publishing's credential security with GitHub's human-in-the-loop protection at exactly the right point.

Migration Pitfalls

A few sharp edges that catch teams:

First, the pending publisher flow. If the PyPI project doesn't exist yet (you're publishing the very first release), you use a pending publisher record that's matched when the project is created. Forgetting this step and configuring a trusted publisher for a project that doesn't exist yet produces a confusing error.

Second, renamed repositories. PyPI's trust record is keyed on the literal owner/repo string. If you rename the repository or transfer it to a new org, the trust record must be updated; it does not follow the rename.

Third, workflow refactoring. Moving the publish step into a reusable workflow or a composite action can change which workflow filename PyPI sees in the OIDC token. Test this in a pre-production project before cutting over the real one.

Fourth, simultaneous token coexistence. It's tempting to configure Trusted Publishing while leaving the old API token in place as a fallback. Don't. The old token remains exploitable, and you've added complexity without subtracting risk. Cut the old token once Trusted Publishing is working.

What It Changes in Your Threat Model

Trusted Publishing does not eliminate all publish-time risk. An attacker who can merge malicious code into the repository and trigger the publish workflow can still ship a malicious release — the credential was never the only control. What Trusted Publishing does eliminate is the class of attacks where a credential leaks from CI, a developer's laptop, a secret scanner, or a backup, and is replayed from somewhere else. That class has been responsible for a meaningful fraction of PyPI compromise events over the past five years.

The attack surface shifts, usefully, from "who has seen the token" to "who can modify the workflow." That's a much smaller set of people, it's auditable through Git history, and it's protected by branch protection rules you probably already have.

How Safeguard Helps

Safeguard detects which of your organization's PyPI publishes use Trusted Publishing versus legacy API tokens and surfaces the gap as a remediation priority. For each of your published projects, Safeguard tracks the configured trusted publisher, flags mismatches between the declared workflow and the actual publish events, and alerts when a publish arrives from an identity that isn't on your trust list. For consumers of PyPI packages, Safeguard records the publishing identity of each release you depend on, giving your security team a full provenance trail even when the publisher isn't your own organization.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.