The Varnish Artifact Firewall is a vendor-neutral firewall for software package registries that enforces policies at request time.
It sits between package clients, such as npm and pip, and package registries. It evaluates each version in a package manifest against your rules, rewrites the manifest before it reaches the client, and refuses downloads of denied versions. Malware, versions with known vulnerabilities, and freshly published packages are stopped before they reach a developer machine or a CI/CD pipeline.
Main features:
deny refuses them with a 403, hide keeps them out of “latest” while a client that pins one still gets it, and allow lets them through.report mode the firewall records each decision without enforcing it, to measure a ruleset’s impact before turning it on.This section runs the firewall on your own machine, in front of the public npm registry, with a ruleset of your own. You need Docker, the npm client, and an Artifact Firewall license. Request a trial license if you do not have one.
Create a project folder with two folders inside it, config/ and rulesets/. Add a ruleset file called rulesets/team-policy.yaml:
id: team-policy
rules:
- id: no-request
match:
- purl: pkg:npm/request
action: deny
reason: request is deprecated, use fetch
- id: hold-uuid-12
match:
- purl: pkg:npm/uuid
version: vers:npm/>=12.0.0
action: hide
reason: uuid 12 and later are ESM only
The first rule refuses every version of request. The second keeps uuid 12 and later out of “latest”, so that npm install uuid gets the newest 11.x release. Artifact Firewall Ruleset describes each field.
Put your license in config/license.lic, and add a configuration file called config/default.yaml:
firewall:
registries:
- name: npmjs
remote_url: https://registry.npmjs.org
routes:
- host: localhost
rulesets:
- path: /rulesets
license:
file: /etc/varnish-artifact-firewall/license.lic
The registries entry sends each request for the host localhost on to the public npm registry. The rulesets entry reads the ruleset files in /rulesets, where the next step mounts your rulesets/ folder.
From your project folder, run:
docker run --rm -p 6089:6089 --name firewall \
-v ./config:/etc/varnish-artifact-firewall:ro \
-v ./rulesets:/rulesets:ro \
varnish/artifact-firewall
The firewall reads its configuration from /etc/varnish-artifact-firewall/default.yaml and listens on port 6089. It loads your two rules and starts serving:
time=... level=INFO msg="loaded rulesets" default_action=allow rulesets=1 rules=2 exact_selectors=2 pattern_selectors=0
time=... level=INFO msg="serving proxy" address=[::]:6089 deployment=standalone
In a second terminal, point npm at the firewall:
export NPM_CONFIG_REGISTRY=http://localhost:6089/
request is refused, and the error names the rule and the ruleset that refused it:
npm view request
npm error code E403
npm error 403 403 Forbidden - GET http://localhost:6089/request - package blocked by firewall (rule "no-request", ruleset "team-policy")
...
uuid resolves to its newest 11.x release, although the latest on the npm registry is in the 14.x line:
npm view uuid version
11.1.1
A client that pins a hidden version, as in npm install uuid@14.0.3, still gets it. Use deny to refuse those too.
The firewall writes each decision to its audit log, on the container’s standard output:
{"id":"01a1c4c5-e35b-711e-baf3-14b0cb5a978e","ts":"2026-11-10T09:24:25.563Z","kind":"listing","purl":"pkg:npm/request","rule_id":"no-request","ruleset_id":"team-policy","action":"deny","enforced_action":"deny","registry":"npmjs","mode":"normal","outcome":"blocked"}
To change the rules, edit the files in rulesets/. A SIGHUP makes the firewall read them again without a restart:
docker kill --signal=HUP firewall
Varnish publishes ready-made rulesets in varnish/osv-rules, generated every hour from the OSV vulnerability database. Together they hold more than 260,000 rules. Over 230,000 of them name packages that were published as malware, from OSV’s MAL- advisories, and the rest name versions affected by a known vulnerability, from CVEs and security advisories.
| Ecosystem | Ruleset |
|---|---|
| npm | rulesets/npm/all.yaml |
| PyPI | rulesets/pypi/all.yaml |
| Maven | rulesets/maven/all.yaml |
| NuGet | rulesets/nuget/all.yaml |
| Go | rulesets/go/all.yaml |
| Hex | rulesets/hex/all.yaml |
Add the npm ruleset to config/default.yaml as a git source. The firewall fetches it again every hour:
firewall:
registries:
- name: npmjs
remote_url: https://registry.npmjs.org
routes:
- host: localhost
default_quarantine_days: 7
rulesets:
- path: /rulesets
- git:
name: osv
url: https://github.com/varnish/osv-rules.git
sub_path: rulesets/npm/all.yaml
license:
file: /etc/varnish-artifact-firewall/license.lic
OSV lists a malicious package once someone has reported it. default_quarantine_days keeps each version out of “latest” for its first seven days, to leave time for that.
A new Git source needs a restart. Stop the firewall with Ctrl+C and run the same docker run command again. The firewall clones the repository before it starts serving:
time=... level=INFO msg="loaded rulesets" default_action=allow rulesets=2 rules=230811 exact_selectors=226510 pattern_selectors=0
A package that was published as malware is now refused:
npm view nodemon-sudo
npm error code E403
npm error 403 403 Forbidden - GET http://localhost:6089/nodemon-sudo - package blocked by firewall (rule "MAL-2026-10014", ruleset "osv-npm")
...
A rule names the package, the affected versions, the advisory, and a severity score:
- id: MAL-2026-10014
aliases:
- GHSA-8228-4gjp-c339
match:
- purl: pkg:npm/nodemon-sudo
severity: 10
reason: Malicious code in nodemon-sudo (npm)
- id: GHSA-2234-fmw7-43wr
aliases:
- CVE-2024-48913
match:
- purl: pkg:npm/hono
version: vers:npm/<4.6.5
severity: 5.9
reason: Hono allows bypass of CSRF Middleware by a request without Content-Type header.
The firewall turns the score into an action with its severity thresholds. On the defaults, a malicious package scores 10 and is refused outright, and the vulnerable versions of hono score 5.9 and are kept out of “latest”, so that a client which does not pin a version gets one that is not affected.
To subscribe to every ecosystem, set sub_path: rulesets, or list several rulesets under sub_paths. To generate the rulesets yourself, from a local copy of the OSV data or with a different action for the few advisories that have no score, run osv-rulegen.
deny, hide and quarantine do in each ecosystem.lodash, rule priorities, and a firewall in front of JFrog Artifactory.