Ansible role  ·  GitHub Action  ·  MIT

Safe package-manager defaults. Proven to be enforcing.

Release age gates, install-script blocking and integrity checks for 14 package managers, so a careless npm install or pip install is hardened without the caller knowing. For servers, AI-agent hosts and CI pipelines. Then a verifier asks every tool what it actually enforces.

On a host
$ ansible-galaxy role install echennells.supply_chain_hardening
In CI
- uses: echennells/supply-chain-hardening/action@v2
Why this exists

You don't control what gets installed. You control the defaults.

An AI agent, a CI job or a teammate reaches for whichever package manager is closest, and the newest version is the one that runs. This project moves the safe choice one level down, into the package managers themselves, so the protection is already there before anyone types install.

48-hour release age gate

Freshly published versions are the ones that get pulled after a compromise. A default cooldown means you are never the first to install them.

Install scripts blocked

Lifecycle hooks are the classic execution path for malicious packages. They are off by default across npm, pnpm, Yarn, Bun, Deno, pip/uv, Composer and NuGet.

Works on tools you haven't installed

Config files are written to their expected paths whether or not the package manager exists yet. Install pnpm next month and the hardened config is already waiting.

What it does

Eight protections across fourteen package managers.

Coverage isn't uniform. Each ecosystem exposes a different set of knobs, and the role applies the strictest one available rather than pretending the gaps aren't there.

Protection npm pnpm Yarn Bun Deno pip/uv Cargo Go Composer Bundler Maven Gradle NuGet
48h release age gate *
Install script blocking
Pre-install reputation (npq)
Socket Firewall
Exact version pinning
Hash / integrity verification
HTTPS-only / source pinning
Lockfile enforcement
*  via the third-party cargo-cooldown crate, through the cargo PATH wrapper Every protection is delivered in up to three layers: system-wide env vars, config files and PATH wrappers. How it works →
Verified, not assumed

Writing a config file is not a protection.

Every real failure in this project's history had the same shape: the file was exactly right and the tool quietly ignored it. So the role ships a verifier that asks the tools what they ended up believing, on the real host, against the real installed versions. It runs after every apply and at the end of every CI job.

$ supply-chain-verify Runs on the host
STATUS EVIDENCE    PROTECTION                       DETAIL
OK     PARSED      npm lifecycle scripts blocked    npm reports ignore-scripts=true
GAP    PARSED      yarn age gate                    yarn reports non-integer npmMinimalAgeGate='NaN'
GAP    FUNCTIONAL  npq reputation checks            installed but SUPPRESSED on Node v18.19.1
WEAK   PRESENT     npm PATH wrapper                 wrapper installed; callers bypassing PATH unaffected
Each row says how it was established: FUNCTIONAL means the protection was exercised and observed, PARSED means the tool reported the setting back, PRESENT means only that a file exists, which is why it is counted as WEAK, never as coverage. Exit status is non-zero on any GAP, so it drops straight into a health check. Reading the verifier →
Where it runs

One set of defences. Three places to put them.

The role configures the package managers that are already there; it doesn't install them. Apply it to a bare host, a sandbox or a container image, or drop the Action into a pipeline.

Servers & agent hosts Ansible role
# install the role, apply to this host, check it
ansible-galaxy role install \
  echennells.supply_chain_hardening
ansible-playbook site.yml --limit localhost
supply-chain-verify
Run as your normal user with sudo rights. Tasks escalate themselves. Install guide →
CI pipelines GitHub Action
- uses: actions/setup-node@v4      # toolchains first
- uses: echennells/supply-chain-hardening/action@v2
- run: npm ci                      # protected from here
- uses: echennells/supply-chain-hardening/action/verify@v2
Toolchains first, then harden, then install, then verify. The order is the only thing to get right. Action docs →
Everywhere else harden.sh
# Dockerfile, GitLab, CircleCI, Azure, Buildkite
COPY action/harden.sh /usr/local/lib/
RUN ECOSYSTEMS=npm,pnpm,pip,uv \
    /usr/local/lib/harden.sh --emit=plain
The same script the Action runs, with adapters for each CI system. Examples →
Honest about limits

It raises the default posture. It is not a sandbox.

A sandbox controls what can run. This controls how package managers behave when they do. The two are complementary, and the boundaries are documented rather than papered over.

Anything running as the same user can override it.

Env vars and config files protect against careless installs, not a determined attacker who already has code execution.

Language package managers only.

apt, pacman and the AUR have their own trust mechanisms. Judging this against distro-package risk measures it against a threat it never claimed.

No single file shows the whole posture.

The layers overlap on purpose, so the file you'd grep is often not the one doing the work. supply-chain-verify is the source of truth.

Every known boundary, each one measured on a real host: docs/limitations.md →

Harden it once. Verify it every time.

MIT licensed. Exercised in CI on Ubuntu 22.04, 24.04 and 26.04, and Debian 12.