SPEC 8 — Securing the Release Process

Authors:
Matthew Feickert <matthew.feickert@cern.ch>, Pamphile Roy <roy.pamphile@gmail.com>, Juanita Gomez <juanitagomezr2112@gmail.com>, Seth Larson <sethmichaellarson@gmail.com>, Lars Grüter <lagru@mailbox.org>, Henry Schreiner <HenrySchreinerIII@gmail.com>, Jarrod Millman <millman@berkeley.edu>
History:
https://github.com/scientific-python/specs/commits/main/spec-0008
Endorsed by:
networkx, numpy, pysal, scikit-image, scikit-learn
Discussion:
Comments below

Description#

Open source libraries constitute a significant portion of the world’s digital infrastructure. Securing the open source supply chain is therefore an increasing concern, with sophisticated attacks such as the 2024 xz utils backdoor and malware attacks on PyPI highlighting the need for stronger safeguards. The Python Software Foundation (PSF) has also prioritized this work, including through the creation of the PSF Security Developer in Residence position in 2023.

The Supply-chain Levels for Software Artifacts (SLSA) framework and OpenID Connect (OIDC) standard provide foundations for tools and practices that help projects secure their release processes.

This SPEC offers pragmatic recommendations for publishing release artifacts securely. Specifically, we recommend:

Securely building release artifacts will be covered in a later SPEC. These recommendations complement SPEC 6 — Keys to the Castle.

The implementation examples use GitHub Actions and PyPI, but the general recommendations apply more broadly to release workflows and distribution services. The specific mechanisms depend on the services used.

Badges#

Projects can highlight their adoption of this SPEC by including a SPEC badge.

SPEC 8 — Securing the Release Process
[![SPEC 8 — Securing the Release Process](https://img.shields.io/badge/SPEC-8-green?labelColor=%23004811&color=%235CA038)](https://scientific-python.org/specs/spec-0008/)
|SPEC 8 — Securing the Release Process| 

.. |SPEC 8 — Securing the Release Process| image:: https://img.shields.io/badge/SPEC-8-green?labelColor=%23004811&color=%235CA038
   :target: https://scientific-python.org/specs/spec-0008/
To indicate adoption of multiple SPECS with one badge, see this.

Implementation#

With a focus on securing the release artifact distribution process, the following processes and standards should be adopted.

Document the release process#

The release process should be clearly and fully documented in the developer documentation and describe each step to make a release and the permissions required to do so. It is recommended that this is a dedicated page in the developer section of the documentation website, though providing instructions in a RELEASING.md in the top level of the repository is also a common approach.

Hardening workflow environment permissions#

Separate building from publishing#

Release artifacts should be built in a separate job from the job that publishes them. The build job should upload the completed distributions as workflow artifacts, and the publishing job should only download those artifacts and upload them to a package index, e.g. PyPI. This separation ensures that build and test code does not run with publishing credentials and that the artifacts tested are the same artifacts that are published.

When a workflow checks out the repository, set persist-credentials: false unless later steps explicitly require the checkout credentials.

- uses: actions/checkout@<full action commit SHA> # vX.Y.Z
  with:
    persist-credentials: false

Restrict permissions in CI runners to the minimum required#

To restrict the attack surface area of arbitrary code execution in CI runners, the default runner permissions should be set to none. In the GitHub Actions workflow, this is accomplished by defining the following workflow-level permissions block before any jobs are defined.

permissions: {}

Each job should then grant only the permissions it requires. For example, a build job that checks out a private repository may need contents: read, while the PyPI publishing job needs id-token: write for trusted publishing.

Restrict permitted actions in workflows#

GitHub allows restricting the actions that workflows can use via the repository actions permissions settings at https://github.com/$ORG/$PROJECT/settings/actions. A reasonable default is to select the

option and the suboptions:

Consult Managing GitHub Actions permissions for your repository for more details.

Use GitHub Actions environments#

Use a GitHub Actions environment

environment:
  name: publish-package

and enforce additional review by at least one other release team maintainer to run a GitHub Actions workflow that publishes to PyPI. Additional reviewer requirements can be configured per GitHub Actions environment under https://github.com/$ORG/$PROJECT/settings/environments/ in the “Deployment protection rules” section.

github-actions-environment

Pin GitHub Actions release workflows to their full release commit SHAs#

GitHub Actions must be pinned using the full commit SHA corresponding to the release version being used. Using versions, branches, tags, or abbreviated hashes is susceptible to attacks. Include the corresponding release version in a comment, and update pinned actions regularly through a reviewed process so that related actions remain compatible. Use a tool, like zizmor, to ensure that hashes are actually from the repository and not from a fork.

- uses: actions/some-action@1fe14e04876783b259436247a3898d2fe7d5548f # vX.Y.Z

Dependabot can be used to automatically propose updates to the hashes. It is important that updates are carefully reviewed before being merged.

# .github/dependabot.yml
version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      actions:
        patterns:
          - "*"

Adopt OIDC through the use of PyPI Trusted Publishers#

Trusted Publishers provide a way to securely establish a short-lived authentication token between a project repository and a distribution platform — such as PyPI. This replaces the need to use a long-lived token to authenticate, reducing the security risks associated with authentication tokens (e.g., tokens being compromised or needing to be rotated).

Trusted Publishers can be used in GitHub Actions by using the pypa/gh-action-pypi-publish GitHub Action defaults in a GitHub Actions environment. When used with Trusted Publishing, this action generates and uploads PEP 740 digital attestations by default, so a separate attestation step is not required. The published provenance can be retrieved through PyPI’s Integrity API and verified using tools such as pypi-attestations.

jobs:
  publish:
    name: Publish release to PyPI
    runs-on: ubuntu-latest
    environment: publish-package
    permissions:
      # IMPORTANT: this permission is mandatory for trusted publishing
      id-token: write
    steps:
      # Download distributions built by a separate job here.
      # ...

      - name: Publish distribution to PyPI
        uses: pypa/gh-action-pypi-publish@<full action commit SHA> # vX.Y.Z
        with:
          print-hash: true

Projects that cannot use Trusted Publishing should still generate and publish provenance attestations where their build and distribution services support them. For example, GitHub’s actions/attest-build-provenance can generate SLSA build provenance for artifacts produced by GitHub Actions.

Example workflow#

The following is a complete example of a workflow which can be used as a starting point:

name: publish distributions

on:
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  publish:
    name: Publish Python distributions to PyPI
    # Distributions will have been built in a previous job
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: publish-package
    permissions:
      id-token: write

    steps:
      - name: Download distributions
        uses: actions/download-artifact@<full action commit SHA> # vX.Y.Z
        with:
          name: python-package-distributions
          path: dist/

      - name: Publish distributions to PyPI
        uses: pypa/gh-action-pypi-publish@<full action commit SHA> # vX.Y.Z
        with:
          print-hash: true

Notes#


On this page