Skip to content

Why Use Trusted Publishing for PyPI?

On March 24, 2026, someone uploaded malicious versions of litellm to PyPI. The package gets roughly 95 million downloads per month. The malicious code harvested SSH keys, cloud credentials, and Kubernetes secrets from every machine that installed it, then attempted to deploy persistent backdoors across entire clusters.

No corresponding release existed on GitHub. The attacker uploaded directly to PyPI, bypassing the project’s normal release process. This means one of two things happened: a maintainer’s account was compromised, or a long-lived API token was leaked. Either way, a credential that granted upload access to PyPI ended up in the wrong hands.

Trusted publishing exists to make this class of attack impossible.

The problem with API tokens

The traditional way to publish a Python package is to generate an API token on PyPI, store it as a secret in your CI system (or on your local machine), and pass it to your upload tool. This workflow has several weaknesses:

Tokens are long-lived. A PyPI API token does not expire. Once created, it works until someone manually revokes it. A token leaked in a CI log, a compromised .env file, or a breached developer laptop can be used weeks or months later.

Tokens are portable. A valid token works from any machine, any network, any CI provider. There is nothing tying the token to the context where it was created. An attacker who obtains a token can use it from their own infrastructure, and nothing in the upload will look unusual.

Token scope is often too broad. PyPI lets you scope tokens to a single project, but many developers create account-wide tokens for convenience. A single leaked account-wide token grants upload access to every package the maintainer owns.

Tokens must be stored somewhere. Every copy of a token is a potential leak. CI secrets, password managers, environment variables, deployment scripts, and shell histories all become attack surfaces.

How trusted publishing works

Trusted publishing replaces stored secrets with identity verification. Instead of “does this request have a valid token?”, PyPI asks “does this request come from an authorized source?”

The mechanism is OpenID Connect (OIDC), the same identity federation protocol used by “Sign in with Google” buttons. GitHub Actions has built-in support for OIDC tokens. Here is what happens during a publish:

  1. You configure a trusted publisher on PyPI, specifying which CI provider, repository, and workflow are allowed to upload your package.
  2. When the CI workflow runs, it requests a short-lived OIDC token from the CI provider (e.g., GitHub). This token contains signed claims about the workflow: which repository it belongs to, which workflow file triggered it, and which branch or tag initiated the run.
  3. The upload tool (e.g., uv publish) sends this OIDC token to PyPI’s token-minting endpoint.
  4. PyPI verifies the OIDC token’s signature against the CI provider’s public keys and checks that the claims match the trusted publisher configuration. If everything matches, PyPI mints a short-lived, scoped API token and returns it.
  5. The upload tool uses that short-lived token to upload the package through the normal upload API. The token expires within minutes.

No secret is stored in CI. No token exists between workflow runs. The credential is created on-demand, scoped to a single job, and expires within minutes.

What trusted publishing prevents

The litellm attack succeeded because a credential with upload access existed outside the normal CI pipeline. With trusted publishing:

  • A stolen maintainer password would not have helped. Uploads require an OIDC token from the configured CI provider, not a PyPI login.
  • A leaked API token would not exist. There is no long-lived token to leak.
  • An upload from an attacker’s machine would fail. The OIDC token is tied to a specific GitHub repository and workflow. PyPI would reject a token from any other source.
  • An upload without a corresponding code change would require compromising the CI pipeline itself, a harder target than stealing a stored secret.

What trusted publishing does not prevent

Trusted publishing is one layer of defense, not a complete solution.

Compromised CI pipelines. If an attacker gains the ability to modify your GitHub Actions workflow or inject code into the build process, they can publish malicious packages through the legitimate pipeline. Trusted publishing verifies where the upload came from, not what was uploaded.

Malicious code in the repository. A compromised maintainer who pushes malicious code to the main branch and creates a release will produce a legitimate trusted-publishing upload. Code review and branch protection rules are the defenses here.

Dependency confusion and typosquatting. Trusted publishing protects your package’s upload path. It does nothing about an attacker publishing a similarly-named package under their own account.

For a comprehensive approach to supply chain security, see Bernát Gábor’s Securing the Python Supply Chain, which covers hash pinning, vulnerability scanning, SBOMs, delayed ingestion, and time-based installation constraints alongside trusted publishing.

Adoption

PyPI has supported trusted publishing since April 2023. GitHub Actions, GitLab CI/CD, Google Cloud, and ActiveState are all supported as identity providers. For packages published through GitHub Actions, GitLab CI/CD, or Google Cloud, PyPI also displays provenance attestations on the package page, letting anyone verify the link between a package and its source repository.

Next steps

Get Python tooling updates

Subscribe to the newsletter
Last updated on

Please submit corrections and feedback...