Bulwark is a lightweight, zero-dependency security gateway that sits between your package managers and public registries (PyPI, npm, Maven Central). It inspects every package request against configurable policy rules and blocks anything risky, before it reaches your developers or CI pipeline.
No database, UI, or vendor lock-in; just a single Go binary per ecosystem, a YAML config file, and full control over your software supply chain.
Software supply chain attacks are the fastest-growing threat vector in the industry. From event-stream and ua-parser-js to PyPI malware campaigns, these attacks hit organizations of every size. Threats like the Shai-Hulud virus can reach any developer with a laptop connected to the internet.
The risk is getting worse. AI agents have lowered the barrier to development — great for innovation, but many new developers lack awareness of package management and supply chain security. They pull dependencies without thinking, trusting that "public packages must be safe."
Teams face three choices:
- Do nothing — trust the open-source ecosystem. Fast, but completely unprotected.
- Buy a commercial platform — enterprise artifact repositories and SCA scanners exist. You get controls, but at significant cost with opaque rule engines and vendor lock-in.
- Bulwark — a transparent, self-hosted policy layer you own. Write rules in YAML. Version-control them. Deploy on Friday afternoon. Immediately protect your org.
We built Bulwark as an open-source answer to option 3.
Watch Bulwark automatically apply safety rules to protect your package stream.
Step 1: Start Bulwark
docker-compose -f docker-compose.demo.yml up -dWait ~10 seconds for the container to boot. Check health:
curl http://localhost:18001/healthzStep 2: Configure npm
npm config set registry http://localhost:18001/Step 3: Install a Well-Established Package
npm install lodashThis succeeds. lodash is years old and passes Bulwark's 7-day minimum age check.
Step 4: Try a Known-Malicious Package (Blocked by Policy)
npm install event-streamThis fails. event-stream is on the deny list (compromised in 2018), so Bulwark blocks it before any code reaches your machine.
Step 5: Try a Package with Install Scripts (Blocked by Policy)
npm install bcryptThis fails. bcrypt has native install scripts in every published version, and it isn't in the trusted allowlist. Bulwark strips all those versions, leaving nothing installable. Your policies are enforced at the network level — no potentially malicious scripts execute.
Step 6: Try a Typosquatted Package (Blocked by Policy)
npm install loadshThis fails. loadsh is 1 edit away from lodash, typical typosquat. Bulwark's Levenshtein distance check catches it automatically and blocks the install. Real supply chain attacks use exactly this technique.
Step 7: Try a Brand-New Package
npm install any-package-published-todayThis fails. Even if legitimate, Bulwark's 7-day quarantine window blocks it by default. This prevents zero-day exploits before the community has time to discover them.
To clean up:
docker-compose -f docker-compose.demo.yml down
npm config delete registry # restore default npm registry
# Remove the Docker images built during the demo
docker rmi bulwark-npm:latest bulwark-pypi:latest bulwark-maven:latest 2>/dev/null
# Remove any dangling build cache
docker builder prune -fWhen a package request arrives:
-
Package check: Does the package name match your deny lists? Is it typosquatted? Does it look suspicious? Block immediately if any rule fires.
-
Version filtering: For allowed packages, Bulwark fetches the version list from the upstream registry and filters each version:
- Too new? Block if published < N days ago (quarantine window for zero-days).
- Pre-release? Block alpha/beta/RC if your policy says so.
- Install scripts? Block npm
preinstall/postinstallscripts unless allowlisted. - Regex patterns? Block versions matching custom patterns (e.g., anything with "rc" or "dev").
- Pinned approved? Bypass age/other checks if you've explicitly approved the exact version.
-
Response rewriting: Remove blocked versions from the response. When some versions pass policy, the filtered response is returned normally. When a package is entirely blocked (package-level deny or all versions removed), Bulwark returns HTTP 403 with a
[Bulwark] package: reasonmessage so your package manager displays a meaningful error instead of a confusing "no versions found" message. Direct download blocks (tarballs, artifacts) include the same structured message with the specific version and rule reason. -
Caching: Cache filtered responses in memory (configurable TTL) so repeated requests don't hit the upstream registry repeatedly.
Result: A single Go binary, YAML config, and your package managers now have transparent, auditable supply chain protection.
When to choose Bulwark: You want immediate, transparent control without committing to a commercial platform. You're comfortable without a CVE feed today (layerable later).
When commercial makes sense: You need a constantly-updated CVE feed with SLA-backed updates.
Point your package managers directly at Bulwark. Simplest setup, most transparent.
Developer → npm/pip/mvn → Bulwark → PyPI/npm/Maven Central
Keep your existing enterprise registry (any artifact repository that supports remote/proxy repositories). Reconfigure its remote/proxy to fetch through Bulwark. No developer client changes needed. Feedback welcome — report your findings if you deploy this.
Developer → npm/pip/mvn → Enterprise Registry → Bulwark → PyPI/npm/Maven Central
- PyPI: PEP 691 JSON + HTML simple index,
/pypi/<pkg>/jsonpassthrough, external tarball proxy with allowlist. - npm: Packument filtering, tarball proxy, scoped packages (
@scope/pkg), install script detection. - Maven:
maven-metadata.xmlfiltering, checksum invalidation, artifact policy, SNAPSHOT blocking. - Shared rule engine: Trusted package allowlists, pre-release blocking, age quarantine, license filtering, version pinning, deny lists, regex patterns, namespace protection, typosquatting detection, velocity anomalies, dry-run mode.
- Operational: YAML config, structured logging (log/slog), dynamic log-level API, disk file logging, in-memory TTL cache,
/healthz&/readyzprobes, JSON metrics.
- No prerequisites for pre-built binaries — download and run.
- Go 1.26+ (only if building from source)
- Docker (optional, for containerized deployment)
The fastest way to get started. The installer downloads the correct binary for your platform, installs it, configures your package manager, creates an autostart entry, and applies the best-practices security rules — all in one command.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/Bluewaves54/Bulwark/main/scripts/install.sh | bashWindows (PowerShell):
irm https://raw.githubusercontent.com/Bluewaves54/Bulwark/main/scripts/install.ps1 | iexInstall specific ecosystems only:
macOS / Linux — install only the npm and pypi proxies:
curl -fsSL https://raw.githubusercontent.com/Bluewaves54/Bulwark/main/scripts/install.sh | bash -s -- npm pypiWindows — install only maven:
& { irm https://raw.githubusercontent.com/Bluewaves54/Bulwark/main/scripts/install.ps1 } mavenWhat the installer does:
- Downloads the correct binary for your OS and architecture.
- Copies it to
~/.bulwark/bin/<ecosystem>-bulwark. - Writes the best-practices config to
~/.bulwark/<ecosystem>-bulwark/config.yaml. - Configures your package manager (npm registry, pip index-url, Maven mirror).
- Creates an autostart entry (macOS LaunchAgent, Linux systemd user service, Windows Startup batch).
After installation — reconfiguring rules:
Edit the config file for the ecosystem you want to change:
# npm rules:
nano ~/.bulwark/npm-bulwark/config.yaml
# pypi rules:
nano ~/.bulwark/pypi-bulwark/config.yaml
# maven rules:
nano ~/.bulwark/maven-bulwark/config.yamlThe service restarts automatically on reboot. To apply changes immediately, restart the proxy using the -setup flag or restart the service:
# macOS:
launchctl unload ~/Library/LaunchAgents/com.bulwark.npm.plist
launchctl load ~/Library/LaunchAgents/com.bulwark.npm.plist
# Linux:
systemctl --user restart bulwark-npm.serviceUninstalling:
~/.bulwark/bin/npm-bulwark -uninstall
~/.bulwark/bin/pypi-bulwark -uninstall
~/.bulwark/bin/maven-bulwark -uninstallThe uninstall command restores your original package manager configuration (npm registry, Maven settings.xml backup).
Using the -setup flag directly (if you already have the binary):
./npm-bulwark -setup # Install with best-practices config, or your edited config filePre-built binaries are available for every release on the GitHub Releases page.
| Platform | Architectures |
|---|---|
| Linux | amd64, arm64 |
| macOS | amd64 (Intel), arm64 (Apple Silicon) |
| Windows | amd64, arm64 |
Quick start — just download and run:
# Download the binary from GitHub or from the command line
curl -LO https://github.com/Bluewaves54/Bulwark/releases/latest/download/npm-bulwark-linux-amd64
chmod +x npm-bulwark-linux-amd64
# Run it — first launch automatically sets up everything:
./npm-bulwark-linux-amd64On first run the binary detects that no config exists, performs a full setup (writes best-practices config to ~/.bulwark/<ecosystem>-bulwark/config.yaml, configures your package manager, and creates an autostart entry), then starts the proxy. No extra downloads or terminal commands needed.
To run in the background (no terminal needed):
./npm-bulwark-linux-amd64 -backgroundThis detaches the process, prints the PID, and logs output to ~/.bulwark/npm-bulwark/daemon.log.
To use a custom config instead of the auto-installed one (requires same format):
./npm-bulwark-linux-amd64 -config my-custom-config.yamlOn macOS use darwin-arm64, on Windows use npm-bulwark-windows-amd64.exe.
Each ecosystem includes a config-best-practices.yaml — a production-ready policy file you can deploy immediately. These configs are curated with real-world supply chain attack data and sensible defaults:
| Config | What's included |
|---|---|
npm-bulwark/config-best-practices.yaml |
38 known-malicious/typosquatted packages blocked, install script deny with allowlist (esbuild, node-gyp, sharp), typosquat detection (Levenshtein) protecting lodash/express/react/axios/etc., 7-day age quarantine, pre-release blocking, trusted scopes (@types/*, @babel/*, @angular/*) |
pypi-bulwark/config-best-practices.yaml |
42 known-malicious/typosquatted packages blocked (colourama, python3-dateutil, noblesse, ctx, etc.), 7-day age quarantine, pre-release blocking, trusted packages (pip, setuptools, numpy*, django*, flask*, requests) |
maven-bulwark/config-best-practices.yaml |
15 malicious/impersonation artifacts blocked (Log4Shell impersonators, namespace squatting on Spring/Apache/AWS SDK), SNAPSHOT blocking, 7-day age quarantine, pre-release blocking, trusted groups (org.apache.*, org.springframework.*, com.google.*) |
Deploy on Friday and immediately protect your org:
cd npm-bulwark
go build -o bin/npm-bulwark .
./bin/npm-bulwark -config config-best-practices.yamlThen configure npm:
npm config set registry http://localhost:18001/
npm install lodash # Works (trusted)
npm install event-stream # Blocked (known malware)PyPI (port 18000):
cd pypi-bulwark && go build -o bin/pypi-bulwark . && ./bin/pypi-bulwark -config config.yaml# ~/.pip/pip.conf
[global]
index-url = http://localhost:18000/simple/npm (port 18001):
cd npm-bulwark && go build -o bin/npm-bulwark . && ./bin/npm-bulwark -config config.yamlnpm config set registry http://localhost:18001/Maven (port 18002):
cd maven-bulwark && go build -o bin/maven-bulwark . && ./bin/maven-bulwark -config config.yaml<!-- ~/.m2/settings.xml -->
<settings>
<mirrors>
<mirror>
<id>bulwark-maven</id>
<mirrorOf>central</mirrorOf>
<url>http://localhost:18002</url>
</mirror>
</mirrors>
</settings># Build
docker build -f npm-bulwark/Dockerfile -t bulwark-npm:latest .
# Run with best practices config
docker run -p 18001:18001 \
-v $(pwd)/npm-bulwark/config-best-practices.yaml:/app/config.yaml \
bulwark-npm:latestOr use the demo Compose setup:
docker-compose -f docker-compose.demo.yml up -dkubectl apply -f k8s/npm-bulwark/
kubectl apply -f k8s/pypi-bulwark/
kubectl apply -f k8s/maven-bulwark/Each proxy reads its own config.yaml. Here's a minimal example:
server:
port: 18001
upstream:
url: "https://registry.npmjs.org"
timeout_seconds: 30
cache:
ttl_seconds: 300
policy:
trusted_packages:
- "react"
- "lodash"
install_scripts:
enabled: true
action: "deny"
allowed_with_scripts: ["esbuild"]
defaults:
min_package_age_days: 3
block_pre_releases: true
rules:
- name: "deny-known-bad"
action: "deny"
package_patterns:
- "event-stream"
- "flatmap-stream"
reason: "Known malicious package"Use config-best-practices.yaml for production-ready defaults — includes trusted package allowlists, install script blocking, and known malware blockers.
| Variable | Description |
|---|---|
PORT |
Override server.port |
BULWARK_AUTH_TOKEN |
Bearer token for upstream auth |
BULWARK_AUTH_USERNAME |
Basic-auth username |
BULWARK_AUTH_PASSWORD |
Basic-auth password |
Bulwark uses Go's log/slog for structured logging with configurable levels, optional disk output, and runtime level changes.
logging:
level: "info" # debug | info | warn | error
format: "text" # text | json
file_path: "/var/log/bulwark/npm.log" # optional; logs also written to this fileWhen file_path is set, log output is written to both stdout and the specified file using io.MultiWriter.
Dynamic log-level changes — every proxy exposes an admin API to adjust the log level at runtime without restarting:
# Get current level
curl http://localhost:18001/admin/log-level
# → {"level":"info"}
# Set level to debug
curl -X PUT http://localhost:18001/admin/log-level \
-d '{"level":"debug"}'
# → {"level":"debug"}Blocked packages are logged with structured fields including the package name, version, rule name, and reason for blocking.
for mod in common npm-bulwark pypi-bulwark maven-bulwark; do
(cd $mod && go test -count=1 -race -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | grep "^total:")
doneAll modules maintain 90%+ statement coverage.
cd e2e/docker && ./run.sh # Linux/macOS
cd e2e\docker && .\run.ps1 # Windows PowerShell
# Individual ecosystems
./run.sh --python-only
./run.sh --node-only
./run.sh --java-onlySee e2e/docker/README.md for details.
All three proxy binaries (npm-bulwark, pypi-bulwark, maven-bulwark) accept the same flags:
| Flag | Description |
|---|---|
-setup |
Install Bulwark with best-practices config and configure the package manager |
-uninstall |
Remove Bulwark and restore the original package manager configuration |
-background |
Start the proxy as a detached background process (no terminal needed). Prints the PID and exits. Output logged to ~/.bulwark/<binary>/daemon.log |
-config <path> |
Path to configuration file (default: config.yaml) |
-auth-token <token> |
Upstream auth bearer token (overrides config) |
-auth-username <user> |
Upstream auth username (overrides config) |
-auth-password <pass> |
Upstream auth password (overrides config) |
Stopping a background process:
# Find the PID (printed when started, or use ps)
ps aux | grep bulwark
# Stop it
kill <PID>Common endpoints (all three proxies):
| Path | Purpose |
|---|---|
GET /healthz |
Liveness probe — always 200 when running |
GET /readyz |
Readiness probe — checks upstream |
GET /metrics |
JSON metrics counters (enabled via config) |
GET /admin/log-level |
Get current log level |
PUT /admin/log-level |
Change log level at runtime (JSON body: {"level":"debug"}) |
PyPI-specific (port 18000):
| Path | Purpose |
|---|---|
GET /simple/{pkg}/ |
PEP 503/691 simple index — returns filtered HTML or JSON |
GET /simple/{pkg} |
Redirects to trailing-slash form |
GET /pypi/{pkg}/json |
PyPI JSON metadata API — filtered releases |
GET /external?url=... |
Proxied tarball download (host allowlist enforced) |
npm-specific (port 18001):
| Path | Purpose |
|---|---|
GET /{pkg} |
Filtered packument (metadata + versions) |
GET /{pkg}/-/{file}.tgz |
Proxied tarball download with version policy check |
Maven-specific (port 18002):
| Path | Purpose |
|---|---|
GET /.../maven-metadata.xml |
Filtered metadata — blocked versions removed from XML |
GET /.../artifact-version.jar |
Proxied artifact download with version policy check |
GET /.../artifact.sha1 |
Checksum sidecar — returns 404 if base file was filtered |
- Architecture & Diagrams — C4 system context, components, deployment sequences.
- Benchmarks — Performance baselines for filtering and rule evaluation.
- Future Enhancements — Roadmap: CVE feeds, distributed caching, observability.
- Contributing — Development workflow, quality gates, conventions.
- Changelog — Release notes and changes.
Apache 2.0 — see LICENSE.