Stop storing PyPI API tokens in CI: use trusted publishing with OIDC
Long-lived PyPI tokens in CI are a standing risk. Trusted publishing uses OIDC claims to grant short-lived upload rights without storing secrets.
26 May 2026, 19:17 UTC

A long-lived PyPI API token stored as a CI secret is a standing, high-value credential. If it leaks through logs, a compromised dependency, or a misconfigured workflow, an attacker can upload malicious releases to your package until someone notices and revokes the token. The useful takeaway is to stop storing the secret at all.
PyPI trusted publishing delegates upload rights to a specific CI workflow via OpenID Connect (OIDC), an identity protocol that lets a CI runner prove which repository and workflow it is running as. PyPI validates the OIDC token's claims — issuer, repository owner and name, workflow filename, optional environment — and grants a short-lived upload session. No static secret is stored in CI.
Why a static token is a problem in CI
API tokens are project-scoped but long-lived. They are typically stored as repository secrets and referenced in release jobs. The token works from anywhere the secret is accessible, and revocation is manual. Rotation is infrequent, so the window of exposure is large.
Trusted publishing removes the secret from the equation. Authentication is bound to the workflow identity, not a reusable token.
How the two-sided match works
Setup is a match between PyPI and the CI job.
On PyPI, a maintainer registers a trusted publisher for the project. The registration records provider, owner, repository, workflow filename, and optionally an environment name. The feature has been generally available since 2023.
In CI, the publishing job must request an OIDC identity token and present claims that exactly match the registration. Claim matching is exact: a typo in owner, repository, workflow filename, or environment name fails authentication with errors that are hard to trace back to the mismatch.
The job needs permission to mint an identity token. In GitHub Actions that is permissions: id-token: write, scoped to the release job only. The publish action exchanges the runner-issued OIDC token for an upload session with PyPI.
A minimal GitHub Actions release job
The example below shows the shape of a release job that builds distributions and publishes without a PyPI token secret. Replace placeholders with your own values. Run this in GitHub Actions on a protected branch or tag.
name: Release to PyPI
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
environment: pypi-release
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: python -m pip install --upgrade build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v3
with:
repository-url: https://upload.pypi.org/legacy/
The environment: pypi-release name is part of the claim match. If you protect the environment with required reviewers, only approved runs can reach PyPI even if the workflow is otherwise runnable.
Risk note: id-token: write lets the job mint OIDC tokens. Scope it to the release job, not the whole workflow, and pin build dependencies to reduce supply-chain risk. Trusted publishing authenticates the workflow, not the correctness of the code.
Hardening, limits, and verification
Practical hardening is to publish from a protected environment and restrict triggers to release tags. Keep project-scoped API tokens as a fallback for local uploads and unsupported CI. Trusted publishing currently covers a limited set of providers, including GitHub Actions and GitLab CI. Local uploads and unsupported CI still need API tokens.
Verify before production:
- Rehearse the workflow against TestPyPI, which also supports trusted publishing.
- Log the runner's OIDC claims in a debug step to confirm repository, workflow filename, and environment values that PyPI will compare against.
- After a test upload, open the release on PyPI and confirm it shows the publishing workflow details, proving the trusted-publisher path was used rather than a token.
Recent versions of the publish action can emit PEP 740 attestations tied to the trusted-publishing flow. Treat attestation defaults as version-dependent and check the action's release notes before pinning or upgrading.
Start by registering a trusted publisher on PyPI for a test project, create a matching release workflow with id-token: write scoped to a protected environment, and validate on TestPyPI. Once claims match, you can remove the PyPI token secret from CI entirely.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.