Eliminating Long-Lived Secrets with PyPI Trusted Publishers
Stop storing long-lived PyPI tokens in your CI/CD secrets. Learn how to use Trusted Publishers and OIDC to establish a secure, identity-based publishing workflow.
24 Oct 2025, 11:19 UTC

The Risk of Static API Tokens
Managing PyPI API tokens in a CI/CD pipeline usually involves storing a long-lived secret in a vault or environment variable. If that secret is leaked—via a compromised runner, a misconfigured log, or an insider threat—an attacker gains the ability to push malicious versions of your package until the token is manually revoked.
The goal is to move from static secrets (something you store) to identity-based trust (something you prove). PyPI's Trusted Publishers feature achieves this by using OpenID Connect (OIDC), allowing your build environment to prove its identity to PyPI without needing a password or token.
How OIDC-Based Publishing Works
Trusted Publishers replaces the PYPI_API_TOKEN with a short-lived identity token. Instead of PyPI checking if a token matches a database entry, it checks if the token was signed by a trusted issuer (like GitHub or GitLab) and if the claims inside that token match your project's specific requirements.
When a workflow runs, the CI provider issues a JWT (JSON Web Token) containing metadata about the run, such as the repository name, the git ref, and the environment. PyPI verifies this token against the configuration you set in your project settings. If the identity matches, PyPI grants a temporary upload session.
Configuring the Trust Relationship
To set this up, you don't write code; you configure the PyPI project settings. You must define the Issuer (e.g., GitHub Actions) and the Identity. The identity acts as a filter to ensure only specific triggers can publish.
- Repository: The exact owner/repo path.
- Workflow: The filename of the YAML workflow (e.g.,
publish.yml). - Environment: A specific GitHub Environment name, providing an extra layer of manual approval if configured.
If any of these filters are too broad, you risk allowing any branch or any workflow in your repo to push to PyPI. If they are too restrictive, your legitimate builds will fail with a 403 Forbidden error.
Worked Example: GitHub Actions Implementation
To use Trusted Publishers, your workflow must have permission to request the OIDC token from GitHub. This is handled via the id-token: write permission.
# .github/workflows/publish.yml
name: Publish Package
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
# This is required to request the OIDC JWT
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build tools
run: pip install build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
# Twine automatically detects the OIDC environment in GitHub Actions
run: twine upload dist/*
Execution Note: Run this in a GitHub Actions runner. No PYPI_API_TOKEN secret should be defined in the repository settings for this to be a true test of Trusted Publishers.
Trade-offs and Limitations
While OIDC significantly improves security, it introduces a dependency on the OIDC provider's availability. If GitHub's identity endpoint is down, you cannot publish, even if PyPI is healthy.
| Feature | Static API Token | Trusted Publishers (OIDC) |
|---|---|---|
| Secret Management | Manual rotation required | No secrets to rotate |
| Attack Surface | Token theft is permanent until revoked | Tokens expire in minutes |
| Compatibility | Works with any tool/runner | Requires OIDC-compatible provider |
Verifying the Setup
To verify the configuration is working without relying on a full release trigger, you can temporarily change the on: trigger to workflow_dispatch. If the twine upload command succeeds without an API token present in the environment, the OIDC handshake was successful.
If the upload fails, check the PyPI project settings to ensure the Workflow name matches the filename exactly and that the Repository string is correct.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.