skip to content

Why is PyPI trusted publishing preferred over a long-lived API token in CI?

level: middleimportance: should knowfreq 35%

answer

  1. One is stored, one is proven
  2. Ask the platform, not the vault
  3. The credential expires in minutes
  4. Repository and workflow are the identity
  5. Configure the publisher before the first upload

basics

~20 s

Trusted publishing has the CI job prove its identity with a short-lived OIDC token and exchange it for an upload token valid for minutes and scoped to one project. No long-lived secret is stored, so nothing can leak or go unrotated.

solid answer

~50 s

An API token is a bearer secret: whoever holds it can publish, it lives until someone revokes it, and it has to be stored in the CI secret store and often on a maintainer's machine as well. Trusted publishing replaces the stored secret with an exchange. The pipeline asks its provider for a short-lived OIDC identity token asserting *which repository, which workflow, and which environment* is running; PyPI verifies that assertion against a publisher you configured on the project and returns a short-lived, project-scoped upload token. Nothing persistent exists to steal. The identity is narrow — rename the workflow file and publishing stops until you update the publisher — and a pull request from a fork cannot obtain it. The tradeoff is that it only works where an OIDC identity exists, so releases cut from a workstation still need a project-scoped token.

code

console · 1 line
console
python -m twine upload --password "$PYPI_TOKEN" dist/*

go deeper

for a junior

Know that uploads authenticate with an API token rather than your account password, and that a modern pipeline usually holds no token at all because the index trusts the CI job's proven identity instead.

for a middle

Be able to describe the exchange: the job requests a short-lived identity token asserting repository and workflow, the index checks it against a configured publisher and returns a short-lived project-scoped upload token. Contrast that with a stored secret that never expires.

for a senior

Demonstrate the operational judgement: pending publishers for a first release, an environment gate in front of the publishing job, project scoping for the tokens you still need, and the recognition that the credential mechanism does not decide who may cut a release.

for a principal

Own the release-authority model across many repositories: which pipelines may publish, how release workflows are reviewed and protected, whether humans may publish at all, and what the standing answer is for systems with no identity provider.

## Two ways to prove you may publish PyPI will not take an account password for an upload. It takes either an **API token** or a token obtained through **trusted publishing**, and the difference is the difference between a stored secret and a proven identity. ### The API token An API token is a bearer credential. You create it on the index, choose whether it is scoped to your whole account or to a single project, and from then on anything that holds the string can publish. twine sends it in the password field, paired with a fixed reserved username rather than your account name. Its properties are exactly the properties of a long-lived secret: * It must be **stored** somewhere — a CI secret store, a config file on disk, a password manager, and in practice several of those at once. * It does **not expire**. It is valid until a human revokes it, which means an unused token from a maintainer who left three years ago is still a live publishing credential. * It is **transferable**. It carries no information about who is using it or from where, so a copy taken from a build log, a compromised laptop or a misconfigured job works identically to the original. * Scoping is the only real mitigation, and it is coarse: project-scoped rather than account-scoped is a large improvement and should always be the default. ### Trusted publishing Trusted publishing inverts the flow. Instead of the CI job holding a secret, it holds nothing, and at publish time it asks its own platform for a **short-lived OIDC identity token**. That token is a signed assertion by the CI provider: this job is running in this repository, from this workflow file, in this environment, on this ref. The job posts it to PyPI. PyPI checks the signature, then checks the claims against a **trusted publisher** you configured on the project page — owner, repository, workflow filename, and optionally a deployment environment name. If it matches, PyPI mints a short-lived (minutes, not months) API token scoped to that one project and returns it, and twine or `uv publish` uses it for the upload. Minutes later it is worthless. Nothing long-lived is stored, so there is no secret to leak from a log, no rotation schedule, and no shared credential that outlives the person who created it. The credential is also *bound to a context*: it cannot be replayed from a laptop, and a workflow triggered by a pull request from a fork cannot obtain it, which closes the "attacker opens a pull request that publishes" path. ## The operational details that bite * **Set-up order.** The publisher is configured on PyPI *before* the first upload. For a project that does not exist yet, you configure a **pending publisher**, which claims the name on the first successful publish. Otherwise the very first release has a chicken-and-egg problem. * **The workflow filename is part of the identity.** Renaming or moving the publishing workflow breaks publishing until the publisher is updated. That is a feature — it is what makes the identity narrow — but it surprises people once. * **The job needs permission to mint an identity token.** On a provider that gates this, the publishing job must be granted the identity-token permission explicitly; without it the exchange fails before it reaches the index. * **Add an environment.** Configuring the publisher with a deployment environment lets you put approval rules and restricted branches in front of the only job that can publish, which narrows "anyone who can push to the repository can cut a release" to "anyone who can push through the protected release path". * **It does not cover everything.** A maintainer publishing from a workstation, or a CI system with no OIDC identity, still needs a token — make it project-scoped, and delete it when the pipeline takes over. ## What it does and does not solve Trusted publishing removes a *stored credential* from the equation. It does not settle the question of who may cause a publish: whoever can change the release workflow, or push the tag that triggers it, can still ship a release. The defence there is repository controls — protected branches and tags, required review on the workflow file, an environment gate — not the credential mechanism. And what to do once a bad release is out, or once a credential is believed compromised, is incident response, a separate discipline with its own playbook: revocation alone is not the whole answer, because anything the credential already published stays published. The one-line summary for an interview: **an API token is something you have and must protect forever; trusted publishing is something you are, proven at the moment of use and useless a few minutes later.**

  • If trusted publishing stores no secret, what stops a colleague from publishing whatever they like?
    Nothing about the credential does. The publisher is bound to a repository and a workflow, so anyone who can change that workflow or trigger it can cause a release. The controls are repository-side: protected branches and tags, required review on the release workflow, and binding the publisher to a deployment environment with approval rules so only a gated job reaches the exchange.
  • Why does configuring a pending publisher matter for a brand-new project?
    A trusted publisher is configured on a project, but a project only exists on the index after its first upload. A pending publisher is configured against a name nobody has claimed yet; the first successful publish through it creates the project and converts the pending entry into a normal publisher. Without it you would have to make the first release with a token and then switch.
  • When is a project-scoped API token still the right answer?
    When there is no OIDC identity to prove: a maintainer releasing from a workstation, a self-hosted runner or CI system without an identity provider, or a private index that does not implement the exchange. Scope the token to the single project, store it only in the secret store, and revoke it once a pipeline takes over publishing.

A stored token is a spare key left under the mat forever; trusted publishing is showing a guard a photo ID and being issued a visitor badge that stops working at lunchtime.

saying these in an interview costs you the question

  • Calling trusted publishing a token stored more securely
  • Thinking the exchanged upload token is long-lived
  • Assuming an account-scoped token is as safe as project-scoped
  • Believing trusted publishing controls who may trigger a release
  • Expecting a fork's pull request to obtain the publishing identity
  • Configuring the publisher only after the first upload fails

context