Skip to content

chore: rolling uv exclude-newer supply-chain quarantine - #69

Merged
tonyandrewmeyer merged 6 commits into
canonical:masterfrom
tonyandrewmeyer:ci/uv-hardening
Jul 7, 2026
Merged

chore: rolling uv exclude-newer supply-chain quarantine#69
tonyandrewmeyer merged 6 commits into
canonical:masterfrom
tonyandrewmeyer:ci/uv-hardening

Conversation

@tonyandrewmeyer

@tonyandrewmeyer tonyandrewmeyer commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Adds a rolling package-level cooldown in [tool.uv]:

exclude-newer = "7 days"

uv refuses to resolve against any package published in the last 7 days, so a compromised release has a window to be caught / yanked upstream before it can enter this project. Complements (does not replace) the Dependabot cooldown, which only protects Dependabot-authored PRs — exclude-newer also covers manual uv add, uv lock regens, uvx bootstraps, and CI re-resolves, which Dependabot cooldown alone doesn't reach.

claude and others added 5 commits July 2, 2026 18:01
Add --locked to every project-scoped `uv run` invocation in CI-invoked
workflows and Makefile targets (security.yaml's pip-audit step, and the
Makefile lint target used by lint.yaml).

Without --locked, if a PR widens a version range in pyproject.toml
without regenerating uv.lock, uv silently re-resolves at CI-run time
and the transitive-dependency delta never appears in the diff for
review. --locked makes uv fail instead, forcing a fresh uv.lock commit
so the resolution change is reviewable.

--locked is used instead of --frozen: --frozen skips the freshness
check against pyproject.toml entirely and just uses whatever lock file
is present, so it would not catch this case.

Out of scope, left unchanged:
- .github/workflows/zizmor.yaml's `uvx zizmor@v1.23.1 ...` -- uvx tool
  invocations are explicitly excluded from this sweep.
- .github/workflows/security.yaml's `uv export --frozen ...` -- this is
  `uv export`, not `uv run`/`uv sync`, so it's out of scope for this
  change; --frozen there is also the correct flag since export
  legitimately should not trigger a re-resolve.
- Makefile's `format` target -- local-dev-only, not invoked by CI.
Add [tool.uv].exclude-newer = "7 days" so every uv resolution ignores
package releases published in the last 7 days.

Dependabot's cooldown setting only protects Dependabot-authored PRs.
It does nothing for every other uv resolution path in this repo:
manual `uv add`, ad-hoc `uv lock` regenerations, `uvx` bootstraps, and
any CI re-resolve. exclude-newer covers all of those with a rolling
7-day quarantine on newly published versions/files, giving a short
window for a bad or compromised release to be caught and yanked
before this project can pick it up.

"7 days" uses uv's friendly duration syntax and is evaluated relative
to the current time at resolution, so the window rolls forward
automatically -- no maintenance required.

Reference: Canonical Security "How-To: Secure a repo" -- Minimum
release age.
Add [tool.uv].no-build = true so uv refuses to build or install from
any source distribution (sdist) -- for this project's own dependencies
and any transitive dependency -- and will only accept prebuilt wheels.

Installing an sdist runs its setup.py / PEP 517 build hooks at install
time, which is arbitrary code execution under the control of whoever
published that package. Wheels carry no such hook and cannot execute
code during install. no-build closes this vector fleet-wide by making
uv error out instead of silently falling back to a source build.

Verified 2026-07-02 that 0/571 dependencies across the fleet's uv.lock
files are sdist-only, so this is a zero-cost adoption today.

If a future dependency genuinely has no wheel available, the narrower
escape hatch is [tool.uv].no-build-package = ["specific-pkg"] rather
than reverting this setting.

Reference: Canonical Security "How-To: Secure a repo" -- Install
scripts.
Replace the `[tool.uv]` block with the canonical fleet comment
(rolling quarantine rationale), drop `no-build = true`, and drop
`--locked` from CI workflow `uv run` / `uv sync` invocations.

See canonical/pytest-jubilant#98 for the exemplar PR and
canonical/charm-tech#22 (references/decisions.md) for the recorded
deferral of `no-build` and `--locked`: uv has no allow-list to
exempt the workspace project from `no-build`, and rolling
`exclude-newer` is fundamentally incompatible with `--locked`
(uv records the resolved-at-lock-time timestamp in `uv.lock`, so
every day CI recomputes a different value and `--locked` errors).
Rolling `exclude-newer` is the surviving pattern.
The rolling '[tool.uv].exclude-newer = "7 days"' resolves to now() - 7
days at each invocation and uv records the absolute timestamp in
uv.lock. Every run the newly-computed floor differs from the stored
one, so 'uv sync --locked' re-resolves, sees a config change, and
errors. Rolling exclude-newer and --locked are fundamentally
incompatible; exclude-newer is the security control with unique
coverage (paths Dependabot cooldown doesn't reach), so --locked has to
go.
@tonyandrewmeyer
tonyandrewmeyer marked this pull request as ready for review July 4, 2026 04:26
@tonyandrewmeyer
tonyandrewmeyer merged commit 7636c2c into canonical:master Jul 7, 2026
7 checks passed
@tonyandrewmeyer
tonyandrewmeyer deleted the ci/uv-hardening branch July 7, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants