DevSecOps

Signing Python Wheels in Production

PyPI supports attestations now. Here is how to actually sign Python wheels in a CI pipeline, verify them at install time, and deal with the rough edges.

Nayan Dey
Senior Security Engineer
6 min read

PyPI shipped Trusted Publishers in 2023 and PEP 740 attestations in 2024. If you publish Python packages and you have been waiting for signing to actually work, the wait is over. The tooling is not perfect, the ecosystem coverage is not universal, and the verification story on the consumer side is still maturing — but if you are responsible for a published Python package, you should be signing it.

This is a practical guide. I have set this up for multiple packages and I will call out the parts where I got stuck.

What PEP 740 Actually Gives You

PEP 740 defines a way to attach cryptographic attestations to files uploaded to PyPI. The attestations are Sigstore bundles — which means they rely on Sigstore's Fulcio CA for short-lived certificates bound to OIDC identities, and Rekor for transparency logging. The identity is typically a GitHub Actions workflow (via OIDC), though any Sigstore-compatible OIDC issuer works.

What this gives you concretely: when a user fetches mypackage-1.0.0-py3-none-any.whl from PyPI, they can also fetch an attestation that says "this exact artifact was built by github.com/acme/mypackage/.github/workflows/release.yml on commit abc123, and that fact is recorded in the public Sigstore transparency log." An attacker who compromises PyPI credentials but not your GitHub Actions cannot forge the attestation.

This replaces the older PGP signing that PyPI deprecated in 2023. Nobody was using PGP; the key distribution problem made it effectively worthless.

The GitHub Actions Pattern That Works

The canonical pipeline uses pypa/gh-action-pypi-publish with attestations enabled. The minimal configuration:

jobs:
  publish:
    permissions:
      id-token: write
      contents: read
    environment: pypi
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python -m pip install build
      - run: python -m build
      - uses: pypa/gh-action-pypi-publish@release/v1
        with:
          attestations: true

The id-token: write permission is mandatory — that is how the action gets an OIDC token to present to Sigstore. The environment: pypi is how you scope Trusted Publishers on the PyPI side. You configure the Trusted Publisher in PyPI's web UI, pointing it at your repository, workflow filename, and environment name. No API tokens are stored in GitHub.

The failure modes I have hit:

  • Forgetting id-token: write. The error is opaque. If your release fails at the attestation step with a permissions error, this is almost always it.
  • Using a reusable workflow without also configuring the Trusted Publisher to match the caller workflow path. PyPI checks the job_workflow_ref, not just the workflow.
  • Publishing from a pull request. OIDC tokens from PRs on forks do not carry the expected identity. Gate releases to pushes on a tag.

Verifying Attestations at Install Time

Here is where the ecosystem is still catching up. pip does not verify PEP 740 attestations at install time, as of late 2024. The attestations are fetchable via PyPI's JSON API, but nothing in the default install path checks them.

The tooling that does verify: pypi-attestations (the CLI) can verify an attestation against an expected identity. This is useful in CI — you can add a verification step to your Docker build that fails if a critical dependency's attestation does not match the expected publisher.

pypi-attestations verify pypi --repository django/django django-5.1.3.tar.gz

The practical pattern: pick your top ten most-critical dependencies (the ones that would be catastrophic to supply-chain compromise), add explicit attestation verification for those in your build pipeline, and accept that the long tail remains TOFU for now. This is not perfect. It is strictly better than nothing.

Which Packages Even Support This?

Adoption is partial. As of late 2024, high-profile Python projects that publish with attestations include cryptography, sigstore-python (unsurprisingly), the PyPA tooling itself (pip, build, setuptools), and a growing set of others. Many large packages — NumPy, Pandas, Django — have published releases with attestations as they adopted the new workflow, though not always uniformly across all releases.

You can check whether a specific file has an attestation via pypi.org/simple/<package>/ with the right Accept header, or via the PyPI JSON API. pypi-attestations has a inspect subcommand that dumps the attestation metadata.

Rough Edges Worth Knowing

A few things are not polished yet.

Attestation metadata is limited. PEP 740 currently specifies what is essentially a provenance attestation (who built what). There is no first-class vulnerability attestation or VEX support tied into the publishing pipeline. If you want SLSA-style build provenance with materials, you add slsa-github-generator as a separate step.

Revocation is via Sigstore's transparency log, not a simple revocation list. If you publish a bad attestation, you cannot "unsign." You publish a new version and rely on consumers to upgrade. Plan accordingly; do not publish test attestations against real package names.

Source distributions and wheels get separate attestations. Your pipeline needs to sign both if you publish both.

Mirrors and caches may not pass through attestations. Artifactory, Nexus, AWS CodeArtifact — each handles attestation metadata differently. Some pass it through, some strip it. Test your specific mirror configuration; do not assume.

Private Package Registries

If you publish to a private index rather than PyPI, attestation support depends on your registry. Artifactory has been adding Sigstore support; Nexus has limited support; pypi-server (the FOSS one) has none. For now, the pragmatic move is to sign packages with Sigstore cosign sign-blob against your own policy and distribute the .sig and .crt files alongside the wheel, then verify at install time with cosign verify-blob.

This is more work than the PyPI-hosted path. It works.

The Policy Question: When Do You Enforce?

The easy half is signing your own releases. The harder half is deciding when to require that dependencies be signed.

My recommendation for most teams: start by collecting. Scan every dependency in your lock file for attestation presence and metadata, and add that data to your internal security dashboard. Do not block builds on missing attestations yet — ecosystem coverage is not high enough. After six to twelve months, review which of your critical dependencies consistently sign, and start enforcing for that subset.

A phased approach avoids the common failure where a security team flips the enforcement switch, builds start failing, the pressure to ship overrides the policy, and the whole effort gets rolled back.

How Safeguard Helps

Safeguard inventories attestation coverage across your Python dependency tree as part of the SBOM pipeline, distinguishing packages that publish PEP 740 attestations, packages that use legacy signing, and packages that are unsigned. Griffin AI flags newly introduced dependencies that lack attestations when comparable signed alternatives exist, and drafts the GitHub Actions YAML to add signing to your own package release workflows. Reachability analysis ensures that signed-but-vulnerable dependencies are prioritized based on actual exposure rather than attestation status alone. Policy gates let you enforce "all production deployments must have SBOM-verified attestations for top-ten critical dependencies" without blocking builds on long-tail packages that have not yet adopted the standard.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.