Claude
Skills
Sign in
Back

uv-supply-chain-hardening

Included with Lifetime
$97 forever

Harden a Python project's dependency supply chain by switching Docker builds from pip to uv, pinning every dependency to the currently-installed version with hashes, and adding a release-age gate so freshly-uploaded (possibly compromised) packages can't be pulled in. Use when locking down dependencies, defending against supply-chain attacks, or migrating a Dockerized Python build from pip to uv.

Cloud & DevOps

What this skill does


# uv Supply-Chain Hardening

This skill converts a Python project from a loose `pip install -r requirements.txt`
build into a **locked, hash-verified, release-age-gated** build using
[uv](https://github.com/astral-sh/uv). It is the response to the wave of
supply-chain attacks targeting Python (and especially crypto) projects, where a
maintainer account is compromised and a malicious version is published to PyPI.

The defense has three layers:

1. **Pin every dependency** to an exact version — no floating ranges, so a build
   can't silently pull a newer (compromised) release.
2. **Verify hashes** — every PyPI distribution is checked against a SHA256 in the
   lockfile, so a tampered-with artifact on the registry is rejected.
3. **Release-age gate** — refuse any distribution published in the last N days, so
   a freshly-uploaded malicious version isn't pulled in before the community has a
   chance to catch and yank it.

## When to Use This Skill

- You have a Python project (with `requirements.txt` and/or `pyproject.toml`) whose
  Docker build runs `pip install` against unpinned or loosely-pinned dependencies.
- You want reproducible, tamper-evident builds.
- You're worried about a dependency (or one of its transitive deps) being hijacked.
- You have private Git dependencies and need them to coexist with hash-locking.

## What This Skill Changes

1. **`requirements.in`** (NEW) — human-edited source-of-truth list of direct deps.
2. **`requirements.txt`** (REWRITTEN) — machine-generated, fully-pinned, hashed lock.
3. **`pyproject.toml`** — adds `[tool.uv] exclude-newer` and pins the build backend
   (or a root **`uv.toml`** if the repo has no `pyproject.toml`).
4. **`Dockerfile`** — installs via a digest-pinned `uv` instead of `pip`; private-dep
   token stays a build-time `ARG`/secret (never baked into the image).
5. **`requirements-private.txt`** (NEW, *optional*) — first-party libs installed at
   HEAD when the user wants their own libraries to always track latest (Step 5b).

Nothing about the application code changes — this is purely the dependency pipeline.

---

## Step 0: Establish the Baseline

**IMPORTANT — pin to what is _installed_, not to "latest".** The whole point is
reproducing the environment you've actually been running and testing against. If
you just run `uv pip compile` with no constraints, it resolves to the newest
versions allowed, which can jump you across several releases you never tested
(and, worse, is exactly the surface a supply-chain attack rides in on).

First, confirm uv is available and capture the currently-installed versions:

```bash
# Inside the project's activated virtualenv
uv --version || pip install uv            # or: brew install uv / pipx install uv
pip freeze > /tmp/installed-constraints.txt
```

`/tmp/installed-constraints.txt` is the constraint set that anchors the compile to
exactly what you have installed today.

**Ask the user:**

1. **"How many days should the release-age gate be?"**
   - Default: **7 days**. Long enough that most malicious releases get caught and
     yanked; short enough you still get timely security patches.

2. **"Do you have private Git dependencies?"** (yes/no)
   - If yes, note the token env var name (commonly `CR_PAT` for a GitHub PAT).

3. **"Before we freeze, are there any of your own libraries you want to update
   first?"**
   - Pinning captures a moment in time. If the user maintains internal libs with
     pending fixes, pull those in and re-`pip install` _before_ freezing, so the
     lock captures the intended versions.

4. **"Should your own private libraries be pinned, or always track latest?"**
   - A common, legitimate stance: pin the **third-party** attack surface, but let
     your **first-party** libs (the ones requiring a token) float to the latest
     commit on every build — you review your own code and want fixes without a
     manual SHA bump. If they choose this, those libs do **not** get pinned in the
     lock; they go in a separate `requirements-private.txt` installed at HEAD. See
     **Step 5b** — it changes Steps 1, 2, and 4.

5. **"What Python version does the *container* run?"**
   - You must compile the lock for the container's Python (`--python-version`), not
     your laptop's. A local 3.14 venv resolving for a 3.13 image picks different
     wheels/markers. Read it off the `FROM python:X.Y` line.

---

## Step 1: Create `requirements.in` (source of truth)

`requirements.in` lists only your **direct** dependencies — the things you actually
import — with no version pins (the lock supplies those). This is the file a human
edits; `requirements.txt` is never hand-edited again.

```
# requirements.in — direct dependencies only. Edit this, then recompile
# requirements.txt with:
#   uv pip compile requirements.in --generate-hashes -o requirements.txt
#
# Pins and hashes for the full transitive tree live in requirements.txt.

example-http-client
example-db-driver
some-public-lib

# Private Git dependency — pinned to an immutable commit SHA, not a branch.
# The commit SHA is the integrity anchor (a branch can be force-pushed; a SHA
# can't). ${TOKEN_ENV} is expanded from the build environment at install time;
# never commit a literal token here.
internal-lib @ git+https://${CR_PAT}@github.com/your-org/internal-lib.git@<commit-sha>
```

**CRITICAL replacements:**
- Direct dependency names → the user's actual top-level imports (mine these from the
  existing `pyproject.toml` `dependencies` and/or the un-hashed `requirements.txt`).
- `internal-lib @ git+...@<commit-sha>` → each private dep, **pinned by full commit
  SHA**. If it's currently pinned to a branch or unpinned, resolve the current commit
  (`git ls-remote https://github.com/your-org/internal-lib.git <branch>`) and pin it.
- `${CR_PAT}` → the user's token env var name.

> **Why a commit SHA for Git deps:** Git distributions cannot carry a PyPI-style
> hash in the lock (see Step 5). The commit SHA _is_ the hash — it's the only thing
> anchoring the integrity of a Git dependency, so it is non-negotiable for these.

> **⚠️ Token-baking via a library's OWN `pyproject.toml` (the subtle leak — check
> this every time).** Distinct from the Dockerfile `ENV` leak in Step 4/6. If one of
> your private libraries declares its *own* dependencies as
> `git+https://{env:CR_PAT}@github.com/...` (the hatch `{env:...}` context form),
> **hatchling expands the token at build time into the wheel's `Requires-Dist`
> metadata** — so a live PAT gets frozen into every image built from that lib, even
> with a flawless Dockerfile. `${VAR}` in a *requirements* file is fine (that file is
> never packaged); `{env:VAR}` inside a *package's* `[project.dependencies]` is not.
> **Fix the library:** declare its deps with token-free URLs
> (`git+https://github.com/...`) and let git `insteadOf` supply auth at install —
> this stays compatible with unpinned/always-latest deps. Then **rotate the PAT**,
> since older built images still carry it. Scan for it in Step 6.

---

## Step 2: Compile the Locked, Hashed `requirements.txt`

Compile from `requirements.in`, constrained to the installed versions, with hashes:

```bash
# A raw `pip freeze` includes VCS/editable lines (`pkg @ git+...`, `-e ...`,
# `pkg @ file://...`) that uv rejects as constraints — keep only `name==version`:
grep -E '^[A-Za-z0-9._-]+==[0-9]' /tmp/installed-constraints.txt > /tmp/constraints.txt

uv pip compile requirements.in \
  --generate-hashes \
  --python-version 3.13 \
  -c /tmp/constraints.txt \
  -o requirements.txt
```

- `--generate-hashes` → writes a SHA256 (often several, one per wheel/sdist) for
  every PyPI distribution. This is what makes registry tampering detectable.
- `--python-version <X.Y>` → **resolve for the Python the container runs, not your
  laptop's.** Compiling under a different interpreter (local 3.14, image 3.13) can
  select different wheels/markers. Match the `FROM python:X.Y` in the Dockerfile.
- `-c /tmp/constraints.txt` → **pins 

Related in Cloud & DevOps