PyPI trusted publishing makes workflow identity part of authorization
PyPI can now exchange GitHub Actions identities for short-lived upload tokens. The change removes stored publishing secrets while making repository control an explicit part of release security.
A publishing token stored in a CI system authorizes whichever process can obtain it. Its original owner may have intended one workflow to upload one project, but possession of the credential supplies the authority. Moving that token between machines does not make it describe the machine, repository or workflow using it. PyPI now offers a way to authorize an identity supplied by the automation service itself.
The April 20 trusted-publishing announcement introduces support for GitHub Actions. A maintainer configures which external identity a PyPI project trusts. The workflow obtains an OpenID Connect token and exchanges it for a short-lived PyPI upload token. The upload still uses a credential, but no manually provisioned publishing secret needs to remain stored in the repository settings.
I dislike the administrative complexity surrounding Python packaging. This change addresses a specific source of that complexity. It removes the need to keep a permanent upload credential synchronized with the automation that uses it, while retaining an explicit authorization decision on PyPI.
Identity and upload authority remain separate
The launch-day publishing documentation describes the exchange in separate steps. GitHub supplies an OIDC identity token. PyPI checks that token against a registered publisher and returns an API token. The upload client uses the returned credential in the ordinary upload operation.
That separation matters because an identity token is evidence about a workflow, not universal permission to publish Python packages. PyPI still decides whether the described identity is authorized for a project. A repository that has never been registered as a publisher cannot gain authority merely by obtaining an otherwise valid GitHub token.
The documentation also distinguishes audiences. PyPI expects pypi, while TestPyPI expects testpypi. The requested audience identifies the intended recipient of the identity assertion. This gives the token exchange another boundary beyond the fact that GitHub issued a token.
The short-lived upload token remains sensitive. Someone who obtains it before expiry may be able to use its authority. The OIDC token is sensitive too, because it can be exchanged while valid. The improvement reduces the lifetime and storage burden of credentials. It does not make credentials harmless during the period in which the publishing system accepts them.
This is why the distinction between removing stored secrets and eliminating authentication is useful. The workflow still has to establish who it is. PyPI still has to establish what that identity may do. The service performs those checks for each exchange instead of relying only on a secret issued earlier and kept until somebody rotates or revokes it.
The registered workflow is a release capability
The configuration documentation from the launch requires a repository owner, repository name and workflow filename. An environment can also be part of the configuration. These fields identify the external state to which the project delegates publishing authority.
A workflow filename consequently becomes security-relevant configuration. The security model explains that one workflow cannot simply claim the identity of another differently named workflow. It also explains that replacing or renaming a workflow into the trusted filename changes the situation. The new file occupies the identity that PyPI was told to trust.
Repository write access therefore remains consequential. A contributor who can modify the release workflow can change the code that runs with publishing authority. Even when the workflow file remains unchanged, modifying code it executes can affect the release. Authorizing a repository workflow creates a dependency on the controls governing that repository.
The launch documentation recommends a dedicated GitHub environment with required reviewers. This permits a separate approval boundary for a publishing job. An environment restriction can make the ability to edit repository content insufficient on its own to initiate an approved release.
The division of responsibility is concrete. PyPI recognizes the configured identity and issues an upload token. GitHub controls how the workflow runs and how environment approval works. Maintainers decide which repository changes and release executions those controls allow. A correct token exchange cannot compensate for authorizing the wrong repository or a workflow that executes untrusted code.
Limit identity-token access to the publishing job
The launch instructions show the permission required by the publishing action. This excerpt assumes that the distribution files have already been retrieved into the publishing job.
jobs:
pypi-publish:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
# Retrieve the built distribution files before this step.
- name: Publish distributions
uses: pypa/gh-action-pypi-publish@release/v1
The action can obtain a GitHub identity token because the job has id-token: write. That permission does not itself authorize an upload on PyPI. A registered publisher supplies that part of the relationship. Both the permission on GitHub and the matching configuration on PyPI are necessary.
The documentation recommends declaring the permission at job level. This limits the jobs that can request the identity token. Giving every job the same permission would increase the amount of workflow execution that has access to credentials it does not need for its own task.
The security guidance also recommends building distributions in a separate job. The publishing job can retrieve those artifacts and upload them without running the build process under the same identity-token permission. That reduces the code executed in the job holding publishing capability.
This separation does not prove that an artifact contains the intended code. A build can still produce incorrect or malicious distributions. The narrower claim is that a build step no longer needs access to the identity-token exchange just because its output will eventually be published. Artifact integrity and credential exposure remain related but distinct problems.
Workflow triggers introduce another boundary. The launch security model says that pull requests from forks generally cannot obtain OIDC tokens for upstream workflows, even when they can trigger workflow execution. It separately identifies pull_request_target as an exception requiring particular care. Treating every pull-request event as having identical access would miss that distinction.
This matters when a release workflow shares helper scripts or reusable steps with contribution checks. Code that is appropriate to run for an untrusted contribution does not automatically belong in a job authorized to publish. Moving authentication to workflow identity makes the provenance of executed code part of the authorization analysis. A short token lifetime limits the time available to use a stolen token, but it does not prevent malicious code already executing inside the authorized job from publishing during that interval. Separating jobs and controlling their triggers address a different part of the failure than expiry does.
I prefer this division because the release permission becomes visible in a small piece of workflow configuration. It is possible to inspect which job asks for an identity and which PyPI project trusts it. A stored token can be scoped too, but it does not explain its intended execution context merely by being present in a secret store.
Project ownership and publisher ownership differ
Trusted publishers belong to PyPI projects. The launch security document explicitly says that removing a user from a project does not remove publishers that user registered. This is a consequential lifecycle rule. Ending a human maintainer relationship and ending an external workflow relationship require separate changes.
A project can have several publishers, and a publisher can be registered for several projects. The configuration documentation gives multiple workflows and multi-project repositories as examples. This flexibility avoids forcing every release arrangement into a single repository-to-package mapping, but it also means that the set of authorized publishers needs its own review.
The documentation also supports creating a project through a pending publisher. A maintainer registers the intended project name and workflow before the project exists. On first use, the publisher creates the project and becomes an ordinary publisher.
This removes a bootstrap step that would otherwise require a different upload credential for the first release. The authority to create the project is established in advance through the pending configuration. Subsequent releases use the resulting project publisher, so the workflow does not need one authentication method for its first run and another for later runs.
GitHub Actions is the supported provider at launch. The announcement describes the underlying work as generalizable to other services providing OIDC identities, but that is a direction for further support. The available mechanism today ties a PyPI project to a configured GitHub workflow and uses that relationship to issue temporary upload authority. Its continuing security depends on preserving control of the workflow and reviewing the publishers attached to the project when maintenance responsibilities change.