Supply-chain proxy for package managers β age gates, CVE scanning, and a real-time operator dashboard. Single binary, 7 ecosystems.
developer / CI β escrow proxy β upstream registry
β
policy engine
ββββββββββββββΌββββββββββββββ
age osv publisher Β· popularity
Packages that fail policy are blocked at the proxy level β they never reach the tool. Operators review blocked events in the real-time dashboard and approve with a single click.
| npm | PyPI | Go | Cargo | NuGet | Maven / Gradle | Composer |
|---|---|---|---|---|---|---|
| β | β | β | β | β | β | β |
brew tap jverhoeks/tap
brew install escrowRun as a background service (auto-starts on login):
brew services start escrow
# β http://localhost:7888/dashboard
# Dashboard credentials are in: $(brew --prefix)/var/log/escrow.logbrew services stop escrow # stop
brew services restart escrow # reload configConfig lives at $(brew --prefix)/etc/escrow/escrow.toml β edit it to enable more ecosystems, then restart the service.
The Homebrew formula also installs escrow-cli, the companion tool for routing your development environment's traffic through the proxy. See Routing Traffic to Escrow for setup options.
escrow-cli setup --dry-run # preview system setup
escrow-cli config write # configure all tools globally
escrow-cli status # check proxy + config + firewall statedocker run -p 7888:7888 ghcr.io/jverhoeks/escrow:latestDebug config (all 7 ecosystems, full policy, admin / escrow):
cd docker/
mkdir -p data && cp escrow.debug.toml data/escrow.toml
docker compose up -d
# β http://localhost:7888/dashboard# macOS arm64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-darwin-arm64 -o escrow
chmod +x escrow && ./escrow
# macOS amd64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-darwin-amd64 -o escrow
chmod +x escrow && ./escrow
# Linux amd64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-linux-amd64 -o escrow
chmod +x escrow && ./escrowOn first boot escrow generates escrow.toml with a random dashboard password and prints credentials to stdout.
./escrow # binds to 127.0.0.1:7888 (localhost only)
./escrow --host=0.0.0.0 # listen on all interfaces (team/CI use)
./escrow --config=/etc/escrow/escrow.toml
./escrow --host=0.0.0.0 escrow.toml # flag + positional config pathπ‘ On first boot, credentials are printed to stdout. Save them β or find them in the generated
escrow.toml.
| Ecosystem | Tools | Proxy URL | Config key |
|---|---|---|---|
| npm | npm, pnpm, yarn, bun | http://localhost:7888/ |
npm = true |
| PyPI | pip, uv | http://localhost:7888/pypi/simple/ |
pypi = true |
| Go modules | go | http://localhost:7888/go/ |
go = true |
| Cargo | cargo | http://localhost:7888/cargo/ |
cargo = true |
| Composer | composer | http://localhost:7888/composer/ |
composer = true |
| NuGet | dotnet, nuget | http://localhost:7888/nuget/index.json |
nuget = true |
| Maven / Gradle | mvn, gradle | http://localhost:7888/maven2/ |
maven = true |
Use escrow as a one-step supply-chain gate in any CI pipeline. Add it before your install steps β no other changes needed:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jverhoeks/escrow@v1
with:
ecosystems: 'npm'
min-days: '7'
osv-severity: 'HIGH'
- uses: actions/setup-node@v6
with:
node-version: '20'
- run: npm install --ignore-scripts
# npm automatically uses the escrow registry β no other changes neededEscrow sets NPM_CONFIG_REGISTRY, PIP_INDEX_URL, GOPROXY, etc. automatically so every install command routes through the proxy. The package cache is stored in GitHub Actions cache and restored on every run β warm cache runs require zero upstream calls.
| Input | Default | Description |
|---|---|---|
ecosystems |
npm,pypi,go,cargo |
Comma-separated list to enable |
min-days |
7 |
Age gate threshold |
osv-severity |
HIGH |
Minimum CVE severity to block (off to disable) |
version |
v1.4.1 |
Escrow binary version |
port |
7888 |
Local proxy port |
cache-key-suffix |
`` | Append to cache key for manual busting |
Output: proxy-url β the base URL (http://127.0.0.1:7888).
β Full guide: docs/github-actions.md
Step-by-step guides for global setup, per-project setup, verify, and remove for each tool:
| Tool | Guide |
|---|---|
| npm | docs/quickstart/npm.md |
| pnpm | docs/quickstart/pnpm.md |
| yarn | docs/quickstart/yarn.md |
| bun | docs/quickstart/bun.md |
| pip | docs/quickstart/pip.md |
| uv | docs/quickstart/uv.md |
| go | docs/quickstart/go.md |
| cargo | docs/quickstart/cargo.md |
| composer | docs/quickstart/composer.md |
| dotnet / NuGet | docs/quickstart/dotnet.md |
| maven | docs/quickstart/maven.md |
| gradle | docs/quickstart/gradle.md |
| GitHub Actions | docs/github-actions.md |
| Threat | Protected? |
|---|---|
| β Same-day injection attacks (packages published and spread within hours) | blocked by age gate |
| β Packages with known CVEs (MEDIUM/HIGH/CRITICAL by default) | blocked by OSV scan |
| β Packages from brand-new publisher accounts | flagged by publisher signal |
| β Packages with sudden download spikes (possible hijacking) | flagged by popularity signal |
| β Packages on your explicit blocklist | blocked at allowlist/blocklist check |
| β Air-gap: packages that haven't been reviewed never reach developer machines | proxy-level enforcement |
| β Postinstall hooks in packages that do pass the gate | use ignore-scripts=true per tool |
| β Typosquatting on packages that pass age/vuln gates | not yet implemented |
β Git-protocol npm deps (npm install github:user/pkg) |
bypass the registry entirely |
| β Composer ZIP archives (artifact air-gap) | metadata filtered; archives fetched from CDN |
| β Publisher signal for Go, Cargo, NuGet, Maven | no public API equivalent |
β οΈ Postinstall hooks are the most important gap. Escrow filters packages from manifests but does not strippostinstallhooks from packages that pass. Setignore-scripts=true(npm/pnpm),enableScripts: false(yarn), oronly-binary = [":all:"](uv) on every developer machine.
escrow-cli is a companion tool (installed alongside escrow via Homebrew) that routes your development environment's package traffic through the proxy. Four methods are available β use one or combine several for complete coverage.
| Method | Catches | Root? | Platform | |
|---|---|---|---|---|
| 1 | Global config files | CLI tools reading standard configs | No | All |
| 2 | Local project config | Per-project, checked-in | No | All |
| 3 | Shell / launch env | CLI + GUI apps (VSCode, Zed...) | No | macOS / Linux |
| 4 | Network redirect | Every process, no config needed | Yes | macOS / Linux |
escrow-cli config write # 1. write tool config files globally
escrow-cli config write-env # 3. LaunchAgent / profile.d β covers GUI apps
escrow-cli config write-shell # 3. .zshrc + .bashrc for new terminals
sudo escrow-cli setup # 4. system account + pf anchor (macOS)
sudo escrow-cli fw-enable # 4. network-level redirect rulesWrites per-tool registry config to your home directory. Covers every package manager that honours standard config files.
escrow-cli config write [--ecosystems npm,pypi,go,cargo,nuget,maven,composer] \
[--proxy-url http://127.0.0.1:7888]What gets written:
| Tool | File written |
|---|---|
| npm, pnpm | ~/.npmrc |
| yarn v1 | ~/.yarnrc |
| yarn v2+ | ~/.yarnrc.yml |
| bun | ~/.bunfig.toml |
| pip | ~/.pip/pip.conf |
| uv | ~/.config/uv/uv.toml |
| poetry | PIP_INDEX_URL block in shell profile |
| go | GOPROXY block in shell profile |
| cargo | ~/.cargo/config.toml |
| nuget | ~/.nuget/NuGet/NuGet.Config |
| maven | ~/.m2/settings.xml |
| gradle | ~/.gradle/init.d/escrow-mirror.gradle |
| composer | ~/.config/composer/config.json |
Each file is backed up to <file>.escrow-backup before being written.
escrow-cli config check # show which tools are configured
escrow-cli config restore # restore all backups
escrow-cli config restore --ecosystems npm,pypi # restore specific ecosystems
β οΈ Go: useGOPROXY=http://127.0.0.1:7888/go,offnot,direct. Theofffallback causes builds to fail loudly when escrow is unreachable rather than silently bypassing it.
Writes config files into the current working directory. Useful for per-project opt-in without changing global settings.
cd your-project/
escrow-cli config write-local [--ecosystems npm,cargo,nuget,pypi,composer]Files written in CWD:
| Tool | File |
|---|---|
| npm, pnpm | .npmrc |
| yarn v1 | .yarnrc |
| yarn v2+ | .yarnrc.yml |
| bun | bunfig.toml |
| uv | uv.toml |
| cargo | .cargo/config.toml |
| nuget | nuget.config |
| composer | composer.json |
Go, pip, maven, gradle have no project-local config equivalent β use Method 1 for those.
escrow-cli config check-local # show which local files are configured
escrow-cli config restore-local # restore all local backupsInjects proxy env vars at the OS level so GUI apps (VSCode, Zed, Cursor) and processes launched outside a terminal also see the proxy settings.
escrow-cli config write-env [--ecosystems npm,pypi,go]
# Check what's active in the launch environment:
escrow-cli config check-envWrites ~/Library/LaunchAgents/com.escrow.environment.plist. The agent runs at every login and injects these env vars into the macOS launch environment so every spawned process inherits them β including VSCode, Zed, and bundled runtimes.
escrow-cli config write-shell [--profiles zshrc,bashrc] [--ecosystems npm,pypi,go]
# Activate in the current terminal immediately (no new window needed):
source ~/.zshrc
# Check which profiles have the block:
escrow-cli config check-shell--profiles accepts: zshrc, bashrc, zprofile, bash_profile, profile.
Env vars injected by both commands:
NPM_CONFIG_REGISTRY=http://127.0.0.1:7888/ # npm, pnpm
YARN_REGISTRY=http://127.0.0.1:7888/ # yarn v1
PIP_INDEX_URL=http://127.0.0.1:7888/pypi/simple/ # pip, poetry
UV_INDEX_URL=http://127.0.0.1:7888/pypi/simple/ # uv
GOPROXY=http://127.0.0.1:7888/go,off # go
GONOSUMDB=*Undo:
escrow-cli config restore-env # remove LaunchAgent
escrow-cli config restore-shell # remove shell profile blockThe network backstop: intercepts all TCP connections to registry hosts at the kernel level using pf (macOS) or iptables / nftables (Linux). Catches every process regardless of config files or environment variables.
# Preview what will happen without making changes:
escrow-cli setup --dry-run
# Apply (creates _escrow service account, patches pf.conf, sets up iptables chain):
sudo escrow-cli setup
# Optional: install passwordless sudo so EscrowManager.app can enable/disable without prompting:
sudo escrow-cli setup --sudoerssudo escrow-cli fw-enable [--ecosystems npm,pypi,go,cargo,nuget,maven,composer] \
[--proxy-port 7888] [--proxy-user _escrow]
sudo escrow-cli fw-disableescrow-cli fw-test [--ecosystems npm,pypi]Output:
proxy: β 127.0.0.1:7888 reachable
npm β registry.npmjs.org:443 β proxy
npm ~ npm.pkg.github.com:443 rule loaded, CDN IP rotated (likely OK)
pypi ~ pypi.org:443 rule loaded, CDN IP rotated (likely OK)
ββ redirect confirmed via live TCP test~β pf rule is loaded, CDN IP changed sincefw-enableran (redirect will work when IP aligns again)ββ no rule loaded, runsudo escrow-cli fw-enable
escrow-cli status # pf rules, config files, proxy health
escrow-cli status --json # machine-readablepf and iptables resolve hostnames to IP addresses at rule-load time. This means:
| Limitation | Impact | Mitigation |
|---|---|---|
| CDN IP rotation | Rules stale after TTL expires (proxy.golang.org TTL: 8s) |
Re-run fw-enable after network change |
| HTTP/3 / QUIC | UDP port 443 bypasses TCP redirect | Package managers use TCP today; monitor as HTTP/3 adoption grows |
| VPN split-tunnelling | Corporate VPN may mark registry IPs as "direct", bypassing redirect | Methods 1β3 remain effective |
| New bundled runtimes | Tool that ignores config and bypasses TCP (e.g. custom go binary) | Methods 1β3 provide defence-in-depth |
For complete hostname-based interception immune to IP rotation, a macOS Network Extension (
NETransparentProxyProvider) is the path forward. Seedocs/specs/swift-network-extension-prompt.md.
| Tool | Method 1 (config) | Method 2 (local) | Method 3 (env) | Method 4 (network) |
|---|---|---|---|---|
| npm | β | β | β | β |
| pnpm | β | β | β | β |
| yarn v1 | β | β | β | β |
| yarn v2+ | β | β | β | β |
| bun | β | β | β | β |
| pip | β | β | β | β |
| uv | β | β | β | β |
| poetry | β (env) | β | β | β |
| go | β | β | β | β |
| cargo | β | β | β | β |
| nuget | β | β | β | β |
| maven | β | β | β | β |
| gradle | β | β | β | β |
| composer | β | β | β | β |
| VSCode bundled npm | β | β | β | β |
| Any rogue script | β | β | β | β |
Real-time package event stream with approve/block controls.
Access at http://localhost:7888/dashboard. Credentials are printed on first boot and stored in $(brew --prefix)/var/log/escrow.log.
Approve a blocked package: click β next to any blocked event. Added to
escrow-allowlist.json immediately. No restart needed.
Remove from allowlist: DELETE /dashboard/api/allow with {"ecosystem","name","version"}.
All changes are recorded in the live feed with the operator's username.
Block a package manually: POST /dashboard/api/block. Same format.
All policy lives in escrow.toml. Without a [policy] section escrow proxies
transparently (with a startup warning).
Blocks packages published fewer than N days ago. Catches injection attacks that publish and spread quickly before the community notices.
[policy.age]
min_days = 7 # packages must be at least 7 days old
action = "block" # block | warn | allowmin_days |
Use case |
|---|---|
| 1 | Catch same-day injections |
| 7 | Recommended baseline |
| 30 | High-security environments |
Checks every package version against the Open Source Vulnerability database.
[policy.osv]
min_severity = "MEDIUM" # LOW | MEDIUM | HIGH | CRITICAL
action = "block"Results are cached 24 hours per version.
π‘ Fail-open: If the OSV API is unreachable or returns a non-200 response, the vulnerability signal returns
skipand the package is allowed through. This is intentional β a transient OSV outage should not block all package installs. If you need fail-closed behavior, mirror the OSV database locally or useaction = "warn"so blocks require an explicit allowlist entry rather than automatic OSV approval.
[policy.publisher]
max_account_age_days = 30
action = "warn"For npm: reads _npmUser (set by the registry at publish time). Publisher lookup
results are cached 1 hour per account.
π‘ Fail-open: If the npm registry API is unreachable, the publisher signal returns
skipand the package passes through.
[policy.popularity]
spike_factor = 10.0 # warn if downloads increased >10Γ week-over-week
action = "warn"[policy.pypi]
block_sdist = true # wheel-only; never run setup.py at install timeaction |
Effect |
|---|---|
block |
Removed from manifest/metadata β tools see it as non-existent |
warn |
Allowed through; event logged with WARN status |
allow |
Signal evaluated but never blocks (monitoring mode) |
Click Approve on any blocked event β added to escrow-allowlist.json immediately.
Click Block on any allowed event β added to escrow-blocklist.json.
escrow-allowlist.json:
[
{
"ecosystem": "npm",
"name": "lodash",
"version": "4.17.21",
"reason": "pinned to known-good version, reviewed by security team",
"added_by": "admin",
"added_at": "2026-05-16T14:00:00Z"
}
]"version": "" is a wildcard β approves all versions of the package.
Allowlist is checked before any policy signal. Approved packages bypass all
trust checks and are recorded with signal: override.
Provide a certificate and key to serve HTTPS directly:
[server]
tls_cert_file = "/etc/ssl/escrow.crt"
tls_key_file = "/etc/ssl/escrow.key"Or terminate TLS at nginx/caddy and set X-Forwarded-Proto: https β escrow
detects this and sets Secure on session cookies automatically.
For large .crate or wheel downloads to slow clients, increase the write timeout:
[server]
write_timeout_seconds = 300 # default 120Limit requests per IP on proxy endpoints:
[server]
proxy_rate_limit_per_min = 600 # 0 = disabled (default)Override the upstream URL per ecosystem to point at Nexus, Artifactory, etc.:
[ecosystems]
npm = true
npm_upstream = "https://nexus.corp.internal/repository/npm-proxy/"
pypi = true
pypi_upstream = "https://nexus.corp.internal/repository/pypi-proxy"
go = true
go_upstream = "https://nexus.corp.internal/repository/go-proxy"
maven = true
maven_upstream = "https://nexus.corp.internal/repository/maven-releases"
maven_snapshot_upstream = "https://nexus.corp.internal/repository/maven-snapshots"Maven SNAPSHOT requests (path contains SNAPSHOT) are routed to maven_snapshot_upstream
when set; other requests go to maven_upstream. Without it, all requests share one upstream.
GET /healthz probes each enabled upstream with a 3-second HEAD request:
{
"status": "ok",
"uptime": "2h34m",
"storage_backend": "disk",
"upstream_status": {
"npm": true, "pypi": true, "go": true,
"nuget": true, "maven": true
}
}Returns HTTP 503 with "status": "degraded" if any upstream is unreachable.
Blobs (tarballs, wheels, JARs) are cached permanently β they never expire. Monitor disk usage and plan capacity accordingly:
du -sh ./escrow-cache/blobs/ # how much blob storage is used
find ./escrow-cache/meta/ -name "*.json" | wc -l # number of metadata entriesThere is no built-in eviction. When disk fills, SetBlob fails silently and packages stop being cached (clients still receive them from upstream, but without the cache benefit). The /healthz endpoint returns "cache_writable": false when the cache directory is not writable β wire this to your alerting.
For long-running deployments, periodically clean old metadata files:
find ./escrow-cache/meta/ -name "*.json" -mtime +7 -delete
β οΈ Blobs should not be deleted β they are the cached packages and their keys are content-addressed.
[Unit]
Description=Escrow package proxy
After=network.target
[Service]
ExecStart=/usr/local/bin/escrow --config=/etc/escrow/escrow.toml
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target[storage]
backend = "disk" # disk | s3 | memory
[storage.disk]
path = "./escrow-cache"
[storage.s3]
bucket = "my-escrow-cache"
region = "eu-west-1"
endpoint = "" # blank = AWS; set for MinIO/Ceph
# S3 uploads use temp files, not RAM buffersWhat is cached:
| Content | Backend | TTL |
|---|---|---|
| npm / PyPI / Composer manifests (filtered) | meta | 5 min |
| NuGet registration (filtered) | meta | 5 min |
Maven maven-metadata.xml (filtered) |
meta | 5 min |
Go .mod files |
meta | 24h |
Go list responses |
meta | 5 min |
| OSV vulnerability results | meta | 24h |
| Publisher account lookups | meta | 1h |
| Maven Central version timestamps | meta | 1h |
| npm / PyPI / Cargo tarballs, wheels, .crate | blob | permanent |
Go .zip source archives |
blob | permanent |
NuGet .nupkg files |
blob | permanent |
| Maven JARs, POMs, checksums | blob | permanent |
Concurrent cold-cache requests for the same manifest are deduplicated via singleflight β only one upstream fetch runs regardless of how many clients ask simultaneously.
Event log persistence β add eventlog_path to persist events across restarts:
eventlog_path = "escrow-events.jsonl" # JSONL append file; empty = in-memory onlyEvents are loaded from the file on startup (last 500). New events are appended atomically.
Send a webhook on every block (Slack, Teams, PagerDuty, custom endpoint):
[alerts]
webhook_url = "https://hooks.slack.com/services/..."Payload:
{
"ecosystem": "npm",
"package": "malicious@1.0.0",
"signal": "age",
"reason": "published 0 day(s) ago (minimum: 7)",
"action": "block",
"timestamp": "2026-05-17T14:03:12Z"
}Webhooks are deduplicated per package per signal during a manifest filter β a 200-version package blocked by age sends one webhook, not 200.
For npm, PyPI, Composer, NuGet, and Maven, escrow filters blocked versions from
the package manifest before returning it. The package manager never learns the
version exists β it cannot be installed by --force or by a dependency resolver
fallback. For Go modules, escrow returns HTTP 403 on .info and @latest
endpoints. For Cargo, blocked versions are omitted from the sparse index NDJSON.
request β allowlist β blocklist β age β osv β publisher β popularity β allow/warn/block
Allowlist is checked first and short-circuits all other checks β an allowlist entry
(including wildcard "version": "") bypasses the blocklist and all trust signals.
Blocklist is checked second; it can block packages not on the allowlist. Signals run
in order; the first block decision terminates the pipeline.
- β
HMAC-SHA256 session cookies (HttpOnly,
SameSite=Strict, 24h TTL) - β
Timing-safe credential and HMAC comparison (
crypto/subtle,hmac.Equal) - β Login rate limiting: 10 failures β 15-minute IP lockout
- β CSRF protection: Origin header checked on all mutating endpoints
- β Request body limit: 64 KB on all POST/DELETE endpoints
- β
Security headers:
Content-Security-Policy,X-Frame-Options,X-Content-Type-Options
Escrow does not store, log, or forward authentication tokens. It acts as an
anonymous read-only client to public upstream registries. Private registry
authentication (.npmrc tokens, PyPI API keys) is not affected.
Every package evaluation is recorded in the in-memory event log (last 500 events).
Dashboard allow/block/remove actions are also recorded with the operator's username.
The event stream is available via SSE (/dashboard/api/stream) and REST
(/dashboard/api/events).
Postinstall hooks β Escrow filters packages from manifests but does not strip postinstall hooks from packages that do pass. You still need ignore-scripts=true (npm/pnpm), enableScripts: false (yarn), or only-binary = [":all:"] (uv) on every developer machine. See the per-tool quickstart guides in docs/quickstart/.
Typosquatting on allowed packages β If a package passes the age and vulnerability gates, escrow serves it. Detecting typosquatting requires manual allowlisting or an additional signal not yet implemented.
git dependencies β npm git-protocol dependencies (npm install github:user/pkg) bypass the package registry entirely and are not routed through escrow.
Composer ZIP archives β The Composer handler proxies and filters the Packagist v2 metadata (which versions are visible). However, the actual ZIP archive downloads happen via dist.url values in the metadata, which point directly to Packagist's CDN or GitHub. Composer package archives are NOT routed through escrow and are not cached locally. Metadata air-gap is achieved; artifact air-gap is not. If Packagist CDN is unreachable, Composer installs fail.
Unknown publish times β When a package's publish time cannot be determined (e.g., Maven Central Search API unavailable, old Packagist entries without timestamps), the age gate treats the package as "ancient" and allows it through. This is fail-open by design to avoid blocking legitimate packages during upstream API outages.
Publisher signal β Publisher account age is checked for npm and PyPI only. No equivalent public API exists for Go, Cargo, NuGet, or Maven.
OSV vulnerability scan β When the OSV API is unreachable, the signal returns skip and the package passes through (fail-open). See the OSV section above for details.
git clone https://github.com/jverhoeks/escrow
cd escrow
# proxy server
go build -o escrow ./cmd/escrow
# system configuration CLI (macOS / Linux)
go build -o escrow-cli ./cmd/escrow-cli
go test ./...[server]
host = "127.0.0.1" # 0.0.0.0 or --host flag for all interfaces
port = 7888
log_level = "info" # debug | info | warn | error
write_timeout_seconds = 120 # increase for slow clients downloading large archives
read_header_timeout_seconds = 10 # time to receive full HTTP request headers (Slowloris defense)
idle_timeout_seconds = 120 # keep-alive connection idle limit
tls_cert_file = "" # blank = HTTP only
tls_key_file = ""
proxy_rate_limit_per_min = 0 # requests/min per IP on proxy endpoints; 0 = disabled
[ecosystems]
npm = true
npm_upstream = "" # default https://registry.npmjs.org
pypi = true
pypi_upstream = "" # default https://pypi.org
go = false
go_upstream = "" # default https://proxy.golang.org
cargo = false
composer = false
composer_upstream = "" # default https://repo.packagist.org
nuget = false
nuget_upstream = "" # default https://api.nuget.org/v3
nuget_flatcontainer_url = "" # optional; derived from nuget_upstream for NuGet.org;
# set explicitly for Nexus/Azure Artifacts which use
# different URL schemes (e.g. .../repository/nuget/download)
maven = false # also covers Gradle
maven_upstream = "" # default https://repo1.maven.org/maven2
maven_snapshot_upstream = "" # route -SNAPSHOT paths here; default: same as maven_upstream
[storage]
backend = "disk" # disk | s3 | memory
[storage.disk]
path = "./escrow-cache"
[storage.s3]
bucket = ""
region = "eu-west-1"
endpoint = "" # blank = AWS S3; set for MinIO
[policy]
[policy.age]
min_days = 7
action = "block" # block | warn | allow
[policy.osv]
min_severity = "MEDIUM"
action = "block"
[policy.publisher]
max_account_age_days = 30
action = "warn"
[policy.popularity]
spike_factor = 10.0
action = "warn"
[policy.pypi]
block_sdist = false # true = wheel-only installs
[dashboard]
enabled = true
path = "/dashboard"
username = "admin"
password = "" # generated on first boot
secret = "" # HMAC session-cookie secret; generated on first boot
[alerts]
webhook_url = ""
allowlist_path = "escrow-allowlist.json"
blocklist_path = "escrow-blocklist.json"
eventlog_path = "" # JSONL file for persistent event log; empty = in-memory only