Skip to content

Why Use Trusted Publishing for PyPI?

Most Python packages reach PyPI through a CI pipeline. A GitHub Actions workflow builds the package, then uploads it using an API token stored as a repository secret. That token is the weak link: it never expires, it works from any machine, and anyone who obtains it can upload whatever they want to PyPI.

Trusted publishing replaces that stored token with short-lived, CI-scoped credentials. Instead of “does this request have a valid secret?”, PyPI asks “does this request come from an authorized CI workflow?” The result: there is no long-lived secret to steal. How to publish to PyPI with trusted publishing walks through the setup.

Note

This is not a theoretical risk. On March 24, 2026, an attacker uploaded malicious versions of litellm to PyPI, a package with roughly 95 million monthly downloads. The malicious code harvested SSH keys, cloud credentials, and Kubernetes secrets. No corresponding release existed on GitHub. The attacker uploaded directly to PyPI, bypassing the project’s normal release process. For the full story, see LiteLLM Got Owned, and Your Dependencies Might Be Next.

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

The underlying 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

When a project uses trusted publishing instead of API tokens:

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

Account takeover. An attacker with access to a maintainer’s PyPI login could add a new trusted publisher pointing to a repository they control, then publish through that. Trusted publishing eliminates the risk of leaked tokens but does not protect against full account compromise. PyPI’s mandatory two-factor authentication, phishing-resistant security keys, and PyPI’s organization account features are the defenses for that threat.

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.

How trusted publishing enables attestations

Trusted publishing is also the prerequisite for digital attestations (PEP 740). PyPI only accepts an attestation signed by a trusted publisher identity from GitHub Actions, GitLab CI/CD, or Google Cloud, and it displays the result as a Provenance entry on each file, letting anyone verify the link between a package and the workflow that built it. Trusted publishing proves the upload came from your CI; the attestation makes that proof public and checkable after the fact. How to publish Python packages with digital attestations shows the extra workflow step.

Adoption

PyPI has supported trusted publishing since April 2023. GitHub Actions, GitLab CI/CD, Google Cloud, and ActiveState are all supported as identity providers. If your project has never used CI for releases, Setting up GitHub Actions with uv covers the workflow basics before you add publishing.

Last updated on