Drop Long-Lived PyPI Tokens: A Practical Guide to Trusted Publishing
Long-lived PyPI API tokens in CI are a standing risk. Trusted Publishing uses OpenID Connect to issue short-lived, workflow-scoped credentials automatically—here is how to wire it up and what to watch for.
18 Nov 2025, 10:51 UTC

The Token Problem Worth Solving
Every PyPI API token sitting in a CI secret store is a long-lived credential with a broad blast radius. If it leaks, an attacker can upload releases until someone notices and rotates it. Rotation is manual, and the token is often copied into multiple repositories, making sprawl hard to audit.
The useful takeaway: PyPI Trusted Publishing lets your CI provider prove its identity via OpenID Connect (OIDC), so PyPI issues a short-lived credential scoped to a single workflow. You never store the token, and it expires when the job ends.
How OIDC Trusted Publishing Works
OIDC is a standard way for one system to issue a signed, short-lived statement about another system. In this case, your CI provider mints a token containing claims such as the repository owner, repository name, workflow file, and deployment environment. You register those expected claims on PyPI as a trusted publisher entry.
When a release workflow runs, the uploader reads the ambient OIDC token from the CI runtime and presents it to PyPI. PyPI validates the token's claims against the registered trusted publisher entry. If they match, the upload proceeds. If they do not, PyPI rejects it. No API secret is stored in your CI provider.
PyPI supports multiple OIDC providers, including GitHub Actions, GitLab CI, Google Cloud Build, and Azure Pipelines. The claim mappings are configurable per provider, but the core pattern is the same: bind a CI context to a PyPI project without a shared secret.
A Concrete GitHub Actions Setup
Register the Publisher on PyPI
In your PyPI project settings, open the Publishing section and create a trusted publisher. You provide the provider (for example, GitHub Actions), the repository owner, the repository name, the workflow filename, and optionally an environment name. Copy the normalized project name exactly as PyPI displays it; mismatches caused by normalization are a common source of confusing failures.
Configure the Workflow
The workflow needs permission to request an OIDC token, and the publish step must use an uploader that knows how to exchange that token with PyPI. The example below builds distributions with python -m build and publishes them without a stored API token.
name: Publish to PyPI
on:
release:
types: [published]
permissions:
id-token: write # Required for the runner to mint an OIDC token
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<PINNED_VERSION>
- uses: actions/setup-python@<PINNED_VERSION>
with:
python-version: '3.x'
- name: Install build frontend
run: python -m pip install build
- name: Build source and wheel distributions
run: python -m build
- name: Publish to PyPI
# Use an OIDC-aware uploader; pin to a reviewed version or commit SHA.
uses: pypa/gh-action-pypi-publish@<PINNED_VERSION>
Run python -m build in the CI job after checkout. It produces the dist/ directory containing the source distribution and wheel. The publish step then uploads those artifacts using the OIDC token; no PYPI_API_TOKEN environment variable or secret is required.
Token-Based vs. Trusted Publishing
| Aspect | Long-lived API token | Trusted Publishing (OIDC) |
|---|---|---|
| Secret storage | Required in CI provider | None |
| Credential lifetime | Until manually revoked | Short-lived, per workflow run |
| Scope | Project-wide or upload-only | Bound to repo, workflow, and environment claims |
| Rotation | Manual | Automatic |
| Primary failure mode | Token leak or stale token | Claim mismatch (e.g., name normalization) |
The Trade-Offs You Cannot Ignore
Trusted Publishing does not replace repository permissions. If a contributor can push to the default branch or modify the workflow file, they can trigger a publish. You still need branch protection and code review on the workflow itself.
Name normalization is a silent failure mode. PyPI normalizes project names, so a repository named my-lib may not match a PyPI project named my_lib. The upload fails with an authorization error that does not clearly say "name mismatch." Always copy the normalized name from the PyPI project page into the trusted publisher configuration.
Revocation is manual. If you retire a workflow or rename a repository, the trusted publisher entry on PyPI remains active until you delete it. Stale entries are usable by any workflow that matches the old claims, so audit them after structural changes.
Verifying the Change Before You Trust It
A practical check is to run the workflow from an unregistered context—for example, a pull request branch or a differently named workflow file—and confirm the upload is rejected. Then run it from the registered protected branch and confirm success.
Open the PyPI project settings page and confirm the trusted publisher entry lists the exact provider, workflow, and environment constraints you expect. Finally, review the CI logs to confirm an OIDC token was obtained automatically and that no API token appears in the job's secrets or environment variables.
Make the Switch Deliberately
- Inventory which projects still rely on long-lived PyPI tokens.
- For one low-risk project, register a trusted publisher and pin the workflow actions to reviewed versions.
- Remove the old
PYPI_API_TOKENsecret from CI only after a successful OIDC-based release. - Audit trusted publisher entries quarterly, especially after repository renames or workflow changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.