SlowShield is a supply-chain defence proxy for PyPI, npm, Go modules, Maven (Maven, Gradle, sbt), Cargo (crates.io) and container images (Docker Hub, GHCR, Quay, registry.k8s.io and others). It sits between your developers, CI and production builds and the public registries, and:
- holds new releases back for a configurable number of days (default 7), so the community and the threat feeds have time to notice a compromised version before you install it;
- blocks known-malicious packages from the OpenSSF/OSV and GitHub Advisory malware feeds (HTTP 451, even when failing open);
- detects tampering: every artifact is verified against the registry's published digests and its first-seen fingerprint; if the bytes ever change, the download is aborted mid-stream;
- shows you what you depend on: a fast, read-only web UI with installs, trends, new dependencies, held-back versions and a security timeline — plus OpenTelemetry metrics, logs and traces with ready-made Grafana dashboards and alerts.
pip / uv / poetry / npm / pnpm / yarn / bun / go / maven / gradle / cargo / containerd / docker / podman
│ HTTPS (TLS 1.3, HTTP/2, HTTP/3)
▼
Caddy (TLS, H3) ──────────────── certificates: ACME, your files, or internal CA
│
SlowShield (Python 3.15, Granian)
├─ blocklist OSV + GitHub malware advisories (sync every hour)
├─ release age per file (PyPI, Maven) / per version (npm, Go, Cargo) / per digest (images)
├─ integrity registry digests + trust-on-first-use fingerprint, verified cache
└─ telemetry OTLP → Alloy → Prometheus / Loki / Tempo → Grafana
│ HTTP/2
▼
pypi.org · files.pythonhosted.org · registry.npmjs.org · proxy.golang.org · sum.golang.org
repo1.maven.org · dl.google.com (Google Maven) · plugins.gradle.org · index.crates.io · static.crates.io
registry-1.docker.io · ghcr.io · quay.io · registry.k8s.io · gcr.io · mcr.microsoft.com · public.ecr.aws
Try it on your laptop: one container, plain HTTP on localhost, nothing kept after you stop it.
docker run --rm -p 127.0.0.1:8080:8080 ghcr.io/squirro/slowshield:latestThe dashboard is at http://localhost:8080. In a second terminal, install something through it (in a throwaway
virtualenv, because Homebrew and current Linux Pythons refuse pip installs outside one):
python3 -m venv /tmp/slowshield-try
/tmp/slowshield-try/bin/pip install --index-url http://localhost:8080/pypi/simple/ requestsReleases younger than a week are held back; known malware is refused with HTTP 451. To send pip, uv, npm and
go through it in every new terminal (bash on Linux shown; on macOS bash reads ~/.bash_profile and zsh
~/.zshrc; in fish use set -Ux NAME value):
cat >> ~/.bashrc <<'EOF'
export PIP_INDEX_URL=http://localhost:8080/pypi/simple/
export UV_DEFAULT_INDEX=http://localhost:8080/pypi/simple/
export npm_config_registry=http://localhost:8080/npm/
export GOPROXY=http://localhost:8080/go
EOF
source ~/.bashrcRemove the lines to switch it off.
git clone https://github.com/squirro/slowshield
cd slowshield/deploy/docker
cp .env.example .env # your hostname, TLS mode, optional GITHUB_TOKEN
docker compose up -d
# with the full Grafana stack:
docker compose -f compose.yaml -f compose.observability.yaml up -dCaddy in front handles TLS (ACME, your own certificates, or an internal CA for testing). Podman (rootless
Quadlet) and Kubernetes (Helm) setups live in deploy/podman and
deploy/helm; every variant has an observability flavour. Images:
ghcr.io/squirro/slowshield and ghcr.io/squirro/slowshield-caddy (amd64 and arm64, SBOM and provenance
attached); pin a release or a digest in production.
pip config set global.index-url https://slowshield.example.com/pypi/simple/
export UV_DEFAULT_INDEX=https://slowshield.example.com/pypi/simple/
npm config set registry https://slowshield.example.com/npm/
go env -w GOPROXY=https://slowshield.example.com/go # without ",direct", which would go around SlowShield
# Maven: a mirror in ~/.m2/settings.xml; Gradle: an init script in ~/.gradle/init.d/ (both on the Setup page)
# Cargo: source replacement in ~/.cargo/config.toml, index "sparse+https://slowshield.example.com/cargo/"
# containerd, Docker: /etc/containerd/certs.d/_default/hosts.toml (Docker: /etc/docker/certs.d/_default/) with
# server = "https://slowshield.example.com" and capabilities = ["pull", "resolve"]Coding agents: the SlowShield Agent Skill teaches them the setup and what SlowShield's answers
mean (Claude Code: /plugin marketplace add squirro/slowshield), and https://slowshield.org/llms.txt indexes the guide
as Markdown.
The UI's Setup page renders ready-to-copy snippets for pip, uv, Poetry, PDM, Pipenv, npm, pnpm, Yarn, Bun, Go, Maven, Gradle, sbt, Coursier, Cargo, containerd, Docker, Podman and BuildKit with your URLs. Every ecosystem lives under a path on the one host (docs/design/routing.md); per-ecosystem hostnames are deprecated and removed in 0.1.
What clients see:
| Situation | Index / packument | Direct download (lockfile) |
|---|---|---|
| version older than the delay | listed | 200 (verified, cached) |
| version younger than the delay | hidden (Cargo: marked yanked); latest points at the newest allowed |
403 + Retry-After (Maven: 425) |
| no version old enough yet (brand-new package) | all non-blocked versions (fail-open, recorded) | 200 |
| package or version on the malware blocklist | removed (451 if nothing is left) |
451 with the advisory |
| bytes differ from the registry digest or first-seen fingerprint | — | stream aborted, event recorded, later 451 |
Container images: a tag resolves to the newest digest it has pointed to for the delay, so nginx:latest keeps
working about a week behind. A pinned digest that is too new, or a blocked image, gets 403 with the reason
(docs/design/oci.md).
| Feed | Token | Notes |
|---|---|---|
| OSV / OpenSSF malicious packages | none | full snapshot once, then incremental via modified_id.csv |
| GitHub Advisory Database (malware) | GITHUB_TOKEN (fine-grained PAT, no permissions) |
without a token the feed is off, the UI shows how to enable it, and slowshield_feed_enabled{reason="missing_token"} lets you alert on it |
No feed covers container images; block a repository, tag or digest with [[blocks]] in config.toml, which also
works for every other ecosystem.
Everything works with defaults plus a few environment variables; config.toml (see
config.example.toml) adds exceptions and tuning. Policy changes are applied
live. Reference: docs/configuration.md.
- Containers: Amazon Linux 2023 only (builders included), distroless runtime (no shell, no package
manager, RPM database kept for scanners), non-root UID 65532, read-only root filesystem, all
capabilities dropped,
no-new-privileges, multi-arch (amd64/arm64). - TLS by Caddy: TLS 1.3 only by default, hybrid post-quantum key exchange (X25519MLKEM768), HTTP/3, HSTS.
- UI: strict Content-Security-Policy (no inline script/style), every value escaped, read-only.
- Supply chain of SlowShield itself: locked & hashed dependencies (
uv.lock), digest-pinned base images, SHA-pinned GitHub Actions audited byzizmor --persona=pedantic, Dependabot with a 7-day cooldown, SBOM + provenance attestations on every image.
See docs/security.md and SECURITY.md for the threat model and how to report a vulnerability.
Async Python 3.15 on Granian (Rust HTTP server) with pyreqwest (Rust reqwest/hyper, HTTP/2 upstream) and msgspec. npm packuments are filtered without decoding version manifests; cached artifacts are sent zero-copy. Every release is load-tested against the previous one on the same runner and blocked on regressions — see perf/README.md.
uv sync # creates .venv with Python 3.15
uv run pytest # unit + integration tests (fake registries, no network)
uv run ruff check && uv run ruff format --check && uv run ty check
cp .env.example .env && uv run slowshield serve --config config.example.tomlMore in CONTRIBUTING.md and docs/development.md.
Apache License 2.0 — see LICENSE and NOTICE. If you distribute a modified version, please give it a different name and logo (brand/README.md).