Protect Against Supply-Chain Attacks with Release-Age Cooldown

Fromager’s release-age cooldown policy rejects package versions that were published fewer than a configured number of days ago. This protects automated builds from supply-chain attacks where a malicious version is published and immediately pulled in before it can be reviewed.

How It Works

When a cooldown is active, any candidate whose upload-time is more recent than the cutoff (current time minus the configured minimum age) is not considered a valid option during constraint resolution. If no versions of a package satisfy both the cooldown window and any other provided constraints, resolution fails with an informative error.

The cutoff timestamp is fixed at the start of each run, so all package resolutions within a single bootstrap share the same boundary.

Enabling the Cooldown

Use the global --min-release-age flag, or set the equivalent environment variable FROMAGER_MIN_RELEASE_AGE:

# Reject versions published in the last 7 days
fromager --min-release-age 7 bootstrap -r requirements.txt

# Same, via environment variable (useful for CI and builder integrations)
FROMAGER_MIN_RELEASE_AGE=7 fromager bootstrap -r requirements.txt

# Disable the cooldown (default)
fromager --min-release-age 0 bootstrap -r requirements.txt

The --min-release-age flag accepts a non-negative integer number of days. A value of 0 (the default) disables the check entirely.

Scope

The cooldown applies to both sdist resolution and pre-built wheel resolution — any candidate whose upload-time is more recent than the cutoff is rejected, regardless of whether it is an sdist or a prebuilt wheel.

The following are not subject to the cooldown:

  • Fromager’s internal build and cache wheel servers. These are not used for version selection — they are checked only for already-resolved pinned versions — so the cooldown has no insertion point.

  • Packages resolved from Git URLs. Git timestamps are set by the client, not the server, and cannot be trusted for cooldown enforcement.

Resolution from a private package index (sdist or wheel) depends on upload-time being present in the index’s PEP 691 JSON responses. If the index does not provide that metadata, candidates are rejected under the fail-closed policy described below. Use resolver_dist.min_release_age: 0 to bypass cooldown for packages from indexes that structurally cannot supply timestamps.

Fail-Closed Behavior

If a candidate has no upload-time metadata — whether it is an sdist or a wheel — it is rejected when a cooldown is active. Fromager uses the PEP 691 JSON Simple API when fetching package metadata, which reliably includes upload timestamps for PyPI.org.

For indexes that only implement the PEP 503 HTML API and cannot supply timestamps, use the per-package resolver_dist.min_release_age: 0 override to bypass the cooldown for affected packages rather than disabling it globally.

Note

If you are writing a get_resolver_provider plugin that uses PyPIProvider with a private index that only implements the PEP 503 HTML API, pass supports_upload_time=False to PyPIProvider. This switches the provider from fail-closed to warn-and-skip, so candidates without upload timestamps are skipped with a warning rather than causing resolution to fail.

Example

Given a package example-pkg with three available versions:

  • 2.0.0 — published 3 days ago

  • 1.9.0 — published 45 days ago

  • 1.8.0 — published 120 days ago

With a 7-day cooldown, 2.0.0 is blocked and 1.9.0 is selected:

fromager --min-release-age 7 bootstrap example-pkg

With a 60-day cooldown, both 2.0.0 and 1.9.0 are blocked and 1.8.0 is selected:

fromager --min-release-age 60 bootstrap example-pkg

Per-Package Override

The cooldown can be adjusted on a per-package basis using the resolver_dist.min_release_age setting in the package’s settings file:

# overrides/settings/my-package.yaml
resolver_dist:
  min_release_age: 0   # disable cooldown for this package
  # min_release_age: 30  # or use a different number of days

Valid values:

  • Omit the key (default): inherit the global --min-release-age setting.

  • 0: disable the cooldown for this package, regardless of the global flag.

  • Positive integer: use this many days instead of the global setting.

This is useful when a specific package is trusted enough to allow recent versions, or when a package’s release cadence makes the global cooldown impractical.

Top-Level == Pin Exemption

Top-level requirements that are a single exact == pin (e.g. torch==2.5.1) bypass the cooldown automatically – the operator has explicitly chosen that version.

Wildcard or compound specifiers (for example ==1.* or ==1.0,>0.9) are not exempt.

== specifiers in transitive dependencies are also not exempt; without this distinction a malicious package could pin its own dependencies to bypass cooldown.