Choosing Between Trusted Publishing (OIDC) and Scoped API Tokens for PyPI CI/CD
When automating Python package uploads to PyPI, you must decide between the new Trusted Publishing flow that uses OIDC tokens and the classic scoped API tokens. This guide compares security, setup, and practical steps for GitHub Actions, GitLab, and other CI providers.
28 Aug 2025, 09:21 UTC

Decision: How to Publish Python Packages from CI to PyPI
When a project needs to publish releases automatically, the two supported mechanisms are:
- Trusted Publishing – an OpenID Connect (OIDC) flow that issues a short‑lived token for the CI job.
- Scoped API Tokens – a long‑lived secret that you store in the CI secret store and use with
twineorbuild.
The choice depends on your security posture, CI provider support, and operational convenience.
Constraints to Consider
- CI Provider Compatibility – Trusted Publishing requires a provider that can issue OIDC tokens (GitHub Actions, GitLab CI, Azure Pipelines, etc.). Self‑hosted runners or older providers may not support it.
- Repository Ownership – Trusted Publishing ties the upload to a specific Git repository and workflow file. Renaming the workflow or moving the repo will break the flow.
- Secret Rotation – Scoped tokens need secure storage and periodic rotation. Trusted tokens expire automatically.
- Branch Protection – To avoid malicious uploads, restrict the workflow to protected branches or tags.
- Human vs. Automation 2FA – 2FA is enforced on human accounts, not on OIDC or token uploads. Ensure your CI has appropriate permissions.
Option Comparison
| Feature | Trusted Publishing (OIDC) | Scoped API Token |
|---|---|---|
| Credential Storage | None – token is issued per run | Secret stored in CI (needs rotation) |
| Setup Complexity | Register publisher on PyPI, add id-token: write to workflow | Generate token on PyPI, add to CI secret store |
| Security | Short‑lived token, tied to repo & workflow, no secret sprawl | Long‑lived, global project rights – higher risk if leaked |
| CI Providers | GitHub Actions, GitLab CI, Azure Pipelines, others that support OIDC | Any CI that can store secrets (GitHub, GitLab, Jenkins, etc.) |
| Rollback | Disable publisher on PyPI, revoking all future uploads | Revoke token on PyPI, rotate secret in CI |
| Auditability | Upload logs include CI job metadata | Upload logs show token usage but no job context |
Trade‑Off Summary
- If you use a modern CI that supports OIDC and want to eliminate long‑lived secrets, Trusted Publishing is the safer option.
- If you need to support legacy CI or have a quick prototype, a scoped API token is simpler to implement.
- For teams that already have a robust secret rotation policy, the convenience of Trusted Publishing can outweigh the initial registration effort.
- Always restrict trusted publishers to protected branches and enable branch protection rules to mitigate PR‑based attacks.
Concrete Implementation – Trusted Publishing with GitHub Actions
- Register the publisher on PyPI
- Navigate to
https://pypi.org/manage/project/<your-package>/publishers/. - Click "Add publisher", choose "GitHub Actions", and provide:
- Repository owner (e.g.,
your-org) - Repository name (e.g.,
your-repo) - Workflow file path (e.g.,
.github/workflows/publish.yml)
- Repository owner (e.g.,
- Save the publisher. It will appear as
trusted-publisherin the project settings.
- Navigate to
- Configure the workflow
Below is a minimal
.github/workflows/publish.ymlthat builds and uploads to TestPyPI first, then to PyPI.name: Publish to PyPI on: push: tags: - 'v*.*.*' jobs: build: runs-on: ubuntu-latest permissions: id-token: write # Required for Trusted Publishing contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install --upgrade build twine - run: python -m build - name: Upload to TestPyPI run: twine upload --repository testpypi dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TESTPYPI_TOKEN }} - name: Upload to PyPI (Trusted Publishing) run: twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TRUSTED_PUBLISHER }}Key points:
- The
permissionsblock must grantid-token: write. - PyPI will replace
${{ secrets.PYPI_TRUSTED_PUBLISHER }}with an OIDC token at runtime. - Using
__token__as the username is required for Trusted Publishing.
- The
- Validate on TestPyPI
- Generate a test token on PyPI (Project settings → API tokens → TestPyPI).
- Store it as
TESTPYPI_TOKENin the repository secrets. - Run the workflow and verify the upload succeeds by visiting
https://test.pypi.org/project/<your-package>.
- Switch to production
- Remove the TestPyPI upload step or replace its token with the production token if you prefer a double‑check.
- Commit the workflow. The next tag push will trigger the Trusted Publishing upload to PyPI.
Concrete Implementation – Scoped API Token with GitLab CI
- Generate a scoped token on PyPI
- Navigate to your project’s
API tokenspage. - Choose
Scoped tokenand set the scope topackages:writefor the specific project. - Copy the token; it will not be shown again.
- Navigate to your project’s
- Store the token in GitLab
- Go to
Settings > CI/CD > Variables. - Create a variable named
PYPI_TOKEN, paste the token, and mark it asProtectedandMasked.
- Go to
- Configure
.gitlab-ci.ymlstages: - build - publish build: stage: build image: python:3.12 script: - pip install --upgrade build twine - python -m build artifacts: paths: - dist/ publish: stage: publish image: python:3.12 dependencies: - build script: - twine upload --repository pypi dist/* only: - tags variables: TWINE_USERNAME: __token__ TWINE_PASSWORD: $PYPI_TOKENKey points:
- The
TWINE_PASSWORDvariable pulls the token from the GitLab secret store. - Ensure the job runs only on protected tags to avoid accidental uploads.
- The
- Validate on TestPyPI
- Repeat the same steps as with Trusted Publishing but target
testpypiand use a test token. - Check the upload in the TestPyPI UI.
- Repeat the same steps as with Trusted Publishing but target
Validation Checklist
- Publisher entry – Verify the publisher matches repo owner, name, and workflow file on the PyPI project page.
- OIDC token request – In CI logs, look for a line similar to
Fetching OIDC token from GitHuband a successful 200 response fromhttps://upload.pypi.org/legacy/. - Upload success – The final step should return HTTP 200 and show the new distribution in the PyPI project page.
- Run the same workflow against
TestPyPIbefore enabling production uploads to catch misconfigurations early. - Audit the CI job history to confirm that only protected branches trigger uploads.
Rollback Strategy
- Trusted Publishing – Log in to PyPI, go to the project’s
Publisherstab, and clickDeletenext to the trusted publisher. All future uploads from that workflow will fail. - Scoped API Token – Revoke the token on PyPI’s API tokens page. Remove or mask the secret variable in your CI system.
Final Recommendation
If your CI provider supports OIDC and you want the lowest secret‑management overhead, adopt Trusted Publishing. It offers short‑lived, repository‑bound credentials that reduce the attack surface. If you need broader CI compatibility or prefer a one‑time secret, a scoped API token is still a viable, secure option provided you enforce proper secret rotation and branch protection. Always validate against TestPyPI first, then monitor production uploads for anomalies.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.