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.
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:
- You configure a trusted publisher on PyPI, specifying which CI provider, repository, and workflow are allowed to upload your package.
- 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.
- The upload tool (e.g.,
uv publish) sends this OIDC token to PyPI’s token-minting endpoint. - 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.
- 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. Two-factor authentication, strong passwords, 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.
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
- How to publish to PyPI with trusted publishing for step-by-step setup instructions
- Setting up GitHub Actions with uv for a general introduction to CI with uv
- What is PyPI? for background on the Python Package Index
- PyPI Trusted Publishers documentation for the official configuration reference
Also Mentioned In
Get Python tooling updates
Subscribe to the newsletter