· 7 min read ·

When the Package Manager Is the Attack Surface

Source: hackernews

When a Python developer installs something with pip install requests, the thing they really need to trust is pip itself. The same logic applies to uv. Astral has published a detailed breakdown of their security practices for uv and ruff, and it is worth reading carefully, not because the individual measures are novel but because of what their combination actually achieves for a tool at this level of the stack.

uv handles package resolution, installation, and Python interpreter management for a rapidly growing number of developers. Ruff is embedded in CI pipelines, editor plugins, and pre-commit hooks across a significant fraction of the Python ecosystem. When Astral ships a release, it lands on many machines quickly. That concentration of deployment is exactly what makes their supply chain security posture consequential in a way that most library maintainers’ posture is not.

What PyPI Trusted Publishing Actually Eliminates

The most underappreciated change in Python packaging security over the last few years is PyPI’s Trusted Publisher mechanism. Historically, publishing to PyPI required generating an API token, storing it as a CI secret, and rotating it manually if it leaked. The attack surface was anything that could read your CI environment: a compromised dependency in your build chain, a malicious GitHub Action, or a CI provider with logging enabled on secret values.

Trusted Publishing replaces this with OIDC federation. When a GitHub Actions workflow runs, GitHub’s OIDC provider issues a short-lived token bound to the specific repository, workflow file, branch, and environment. PyPI accepts that token as proof of identity without any long-lived credential being involved. There is nothing to steal that remains useful after the workflow run ends.

PEP 740, which landed in PyPI’s implementation in 2024, extends this further by allowing packages to be accompanied by Sigstore-based attestations at publish time. When a package is published from a Trusted Publisher workflow, PyPI can automatically request a Sigstore bundle signing the distribution and record it alongside the package. Consumers can then verify that a specific wheel was produced by a specific workflow run at a specific commit, using the Sigstore transparency log (Rekor) as the audit trail.

Astral uses both. Their releases go through workflows that generate SLSA provenance and sign artifacts with Sigstore’s keyless model before publishing to PyPI.

What SLSA Level 3 Actually Proves

The SLSA framework (Supply-chain Levels for Software Artifacts) defines a hierarchy of guarantees about build provenance. Level 1 requires machine-generated provenance. Level 2 requires the provenance to be hosted by the build system itself. Level 3 requires a hardened build platform where the build definition cannot be tampered with even by the project’s own contributors.

GitHub Actions with the SLSA GitHub generator achieves Level 3 for artifacts by running the signing step in a separate, isolated workflow job that project contributors cannot modify without triggering a review. The resulting provenance document records the exact commit SHA, the workflow run ID, and the digest of the produced artifact. Anyone with the gh CLI can verify this:

gh attestation verify uv-0.4.0-x86_64-unknown-linux-gnu.tar.gz \
  --repo astral-sh/uv

What this proves is that the artifact was built from a specific commit by a specific workflow run. What it does not prove is that the commit itself was trustworthy, that the build environment was free of malicious toolchain components, or that the developers who merged the commit had not been compromised. SLSA narrows the threat model significantly, but it does not eliminate it.

The xz-utils backdoor in March 2024 is the clearest recent example of the residual threat. Jia Tan, the attacker, did not compromise the release pipeline. They spent two years contributing to the project legitimately, building trust, gaining commit access, and then inserting the backdoor through the normal contribution process. SLSA attestations would have accurately verified that the malicious release was built from the malicious commit; the attestations would have been correct, and the commit was the problem.

This is not an argument against SLSA. It is a reminder that provenance attestations close one specific attack vector, namely someone modifying a binary after it leaves the repository, and leave others open. For Astral’s threat model, where the realistic attack surface is a compromised CI token or a tampered binary in a distribution mirror, SLSA Level 3 provides real protection.

Sigstore’s Keyless Trust Model

The traditional alternative to Sigstore is GPG signing, where a developer generates a long-lived key pair, signs releases with the private key, and publishes the public key to a keyserver. Users are expected to fetch the public key, verify its fingerprint through some out-of-band channel, and check the signature. In practice, almost nobody does this, partly because the UX is poor and partly because the out-of-band verification step is difficult to do correctly.

Sigstore’s keyless model addresses this differently. When a workflow signs an artifact, it requests a short-lived certificate from Fulcio (Sigstore’s certificate authority), which issues a certificate bound to the OIDC identity of the workflow, for example https://github.com/astral-sh/uv/.github/workflows/release.yml@refs/heads/main. The signature is then logged to Rekor, Sigstore’s append-only transparency log. The ephemeral private key is discarded after signing; what persists is the certificate, the signature, and the log entry.

Verification does not require finding and trusting a developer’s GPG key. It requires confirming that the certificate was issued by Fulcio, that the identity on the certificate matches what you expect, and that the log entry exists and matches the artifact. The Sigstore Python library wraps this into a usable interface:

from sigstore.verify import Verifier
from sigstore.verify.policy import Identity

verifier = Verifier.production()
verifier.verify_artifact(
    artifact_bytes,
    bundle,
    Identity(
        identity="https://github.com/astral-sh/uv/.github/workflows/release.yml@refs/heads/main",
        issuer="https://token.actions.githubusercontent.com",
    ),
)

The trust model is rooted in Fulcio’s certificate transparency and Rekor’s tamper-evident log rather than in any individual developer’s key management hygiene. This is a meaningful improvement over GPG’s web of trust model, which in practice degrades to trusting whoever uploaded the key first.

The End-User Verification Gap

The mechanisms above provide genuine guarantees, but only if someone actually checks them. By default, when you run curl -LsSf https://astral.sh/uv/install.sh | sh, you are trusting Astral’s CDN and TLS certificate. When you run pip install uv, you are trusting PyPI’s TLS and its malware scanning. The SLSA attestations and Sigstore signatures exist and are available to be checked, but nothing in the default installation path forces that check.

This gap is not unique to Astral. The same situation exists for pip itself, for Node’s npm, and for Rust’s cargo. The tooling to verify provenance has improved substantially; the default user experience does not use it. Some organizations managing packages in regulated environments have started integrating attestation verification into their procurement workflows, and GitHub’s artifact attestation verification is integrated into some enterprise security scanning tools. For individual developers running uv pip install, however, verification remains opt-in and uncommon.

The open problem is making verification load-bearing by default without making installation unusable. Some proposals involve package managers caching trusted attestation roots and warning on mismatch, similar to how SSH warns on host key changes. Whether this gets traction depends on how much friction the ecosystem is willing to accept in exchange for the guarantee. The Alpha-Omega initiative, funded by OpenSSF, is actively working on this problem for high-criticality packages, and uv qualifies on any reasonable measure of criticality.

The Rust Toolchain Angle

One aspect that does not get enough attention in Python supply chain discussions is that uv is built in Rust, and Rust has its own supply chain story. Astral’s security writeup mentions running cargo audit against the RustSec advisory database, which covers known vulnerabilities in crates. Their CI also pins GitHub Actions to full commit SHAs rather than mutable version tags, which protects against tag-squatting attacks where an upstream action maintainer is compromised and the tag is moved to a malicious commit.

Pinning actions to SHAs is something every project should do and most do not. Tools like Dependabot and Renovate can automate SHA updates while keeping the pinning discipline intact. The reason it matters is that uses: actions/checkout@v4 is a mutable reference; uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 is not. If the v4 tag is moved to a malicious commit, every workflow using the tag reference picks up the malicious code on the next run.

Why a Tooling Vendor’s Posture Matters More

Most open source security writeups are about libraries. Libraries matter, but they are individually narrow in scope. A compromised requests release harms applications that import it directly; a compromised uv release potentially harms every machine where it runs, and uv specifically has the ability to download and execute Python interpreters. The attack surface is qualitatively different from a library that parses HTTP.

Astral’s writeup is worth reading not just as documentation of their specific practices but as a practical template for other tooling maintainers. The individual techniques, SLSA provenance, Sigstore signing, Trusted Publishing, SHA-pinned actions, cargo audit, are all described in OpenSSF guidance and SLSA documentation. What is less common is seeing a team apply all of them coherently to a toolchain that sits this close to the base of the dependency graph, and to explain the reasoning clearly enough that others can follow the same path.

Was this interesting?