Writing Custom Firewall Rules

In the Getting Started tutorial, you ran the Artifact Firewall with the public OSV ruleset for known vulnerabilities. This tutorial shows you how to add your own custom rules on top, for internal forks, deprecated dependencies, or any policy your security team defines.

This tutorial walks you through 4 steps:

  1. Add a local ruleset directory to your config.
  2. Write your first rule.
  3. Test it.
  4. Reload rules without restarting.

Prerequisites

Step 1: Add a local ruleset directory to your config

Open your config/default.yaml and add a path: entry to the firewall.rulesets list:

firewall:
  registries:
  - name: npmjs
    remote_url: https://registry.npmjs.org
    routes:
    - host: localhost
  default_action: allow
  default_quarantine_days: 7
  rulesets:
  - git:
      name: osv-npm
      url: https://github.com/varnish/osv-rules.git
      sub_path: rulesets/npm/all.yaml
  - path: /rulesets

license:
  file: /etc/varnish-artifact-firewall/license.lic

The firewall combines both sources: OSV rules from Git and your custom rules from /rulesets.

Step 2: Write your first rule

Create a folder called rulesets/ next to your config/ folder, then add a file inside it:

# rulesets/express.yaml
id: express-ruleset

rules:
  - id: deny-express
    action: deny
    priority: 10
    reason: "Blocked by policy, use internal Express fork instead"
    match:
      - purl: "pkg:npm/express"   # matches ALL versions

Each rule needs a unique id within its ruleset. The purl field uses the standard Package URL format: pkg:<type>/<name>@<version>. Omitting the version matches every version.

The priority: 10 is what makes your rule win over the OSV ruleset. Rules are evaluated in priority order, and the OSV ruleset ships its rules at the default priority (1). When more than one rule matches a package, the firewall applies the highest-priority rule; if several match at the same priority, the strongest action wins (deny > hide > allow). Giving your custom rules a higher priority guarantees they take precedence over the public OSV rules. See the Ruleset Priority reference for details.

Your folder structure should now look like this:

./
├── config/
│   ├── default.yaml
│   └── license.lic
└── rulesets/
    └── express.yaml

Step 3: Run the container with the new mount

Restart your container, adding a volume mount for the new rulesets directory:

docker run --rm -p 6089:6089 --name firewall \
  -v ./config:/etc/varnish-artifact-firewall:ro \
  -v ./rulesets:/rulesets:ro \
  varnish/artifact-firewall

You should see the firewall load both ruleset sources at startup, the OSV rules and your one rule:

time=... level=INFO msg="loaded rulesets" default_action=allow rulesets=2 rules=230472 exact_selectors=226179 pattern_selectors=0
time=... level=INFO msg="serving proxy" address=[::]:6089 deployment=standalone

Test the rule

In a new terminal, point npm at the firewall by setting it as the default registry:

export NPM_CONFIG_REGISTRY=http://localhost:6089/

This way you don’t need to append --registry=... to every npm command. The --dry-run flag means nothing is installed, and the --prefer-online flag makes npm ask the firewall again rather than answer from its own cache.

npm --verbose pack express --dry-run --prefer-online
npm http fetch GET 403 http://localhost:6089/express 18ms (cache skip)
npm error code E403
npm error 403 403 Forbidden - GET http://localhost:6089/express - package blocked by firewall (rule "deny-express", ruleset "express-ruleset")
npm error 403 In most cases, you or one of your dependencies are requesting
npm error 403 a package version that is forbidden by your security policy, or
npm error 403 on a server you do not have access to.
...

The rule denies the package as a whole, so the firewall refuses the package document itself, and the error names the rule and the ruleset that blocked it. The audit log, in the terminal that runs the container, records the refusal:

{"id":"01a1abe8-b274-7802-a480-dd6093440037","ts":"2026-11-05T13:31:56.404Z","kind":"listing","purl":"pkg:npm/express","rule_id":"deny-express","ruleset_id":"express-ruleset","action":"deny","enforced_action":"deny","registry":"npmjs","mode":"normal","outcome":"blocked"}

Step 4: Update rules without restarting

You can add or change rules at any time, with no container restart needed. Let’s add a rule that hides uuid 12 and later.

Add a new ruleset file

Create rulesets/uuid.yaml:

# rulesets/uuid.yaml
id: uuid-ruleset

rules:
  - id: hide-uuid-12
    action: hide
    priority: 10
    reason: "uuid 12 and later are ESM only, not yet approved"
    match:
      - purl: "pkg:npm/uuid"
        version: "vers:npm/>=12.0.0"

Trigger a live reload

docker kill --signal=HUP firewall

On SIGHUP the firewall reads every ruleset file again and applies what changed, with no downtime and no restart. Its log names the added ruleset:

time=... level=INFO msg="rulesets reloaded" added=[uuid-ruleset] rulesets_changed=1 rules_changed=1 rulesets=3 rules=230473

The admin API’s /api/update reloads the rulesets as well, and fetches the Git sources again too.

Verify the hide behaviour

Even though uuid’s actual latest is in the 14.x line, the firewall steers latest to the highest allowed version, 11.1.1:

npm --verbose pack uuid --dry-run --prefer-online
{"id":"01a1abe8-b3f8-76aa-a0f7-16d34ae7a1f8","ts":"2026-11-05T13:31:56.792Z","kind":"download","purl":"pkg:npm/uuid@11.1.1","rule_id":"default","ruleset_id":"default","action":"allow","enforced_action":"allow","registry":"npmjs","mode":"normal","outcome":"served"}

Hidden versions can still be fetched when pinned explicitly:

npm --verbose pack uuid@14.0.3 --dry-run --prefer-online
{"id":"01a1abe8-b54d-7f7d-b03f-d247fefa8314","ts":"2026-11-05T13:31:57.133Z","kind":"download","purl":"pkg:npm/uuid@14.0.3","rule_id":"hide-uuid-12","ruleset_id":"uuid-ruleset","action":"hide","enforced_action":"hide","registry":"npmjs","mode":"normal","outcome":"served"}

hide silently steers latest resolution to the highest allowed version; pinned fetches of hidden versions still install. Use deny instead if you want to block pinned fetches too.

Rule actions at a glance

Action npm install pkg (latest) npm install pkg@x.y.z (exact)
allow Version is served normally and eligible for latest. Served normally.
hide Excluded from latest; the next allowed version is returned instead. Still served (use deny to block pinned fetches).
deny Excluded from latest and any fetch returns 403. Returns 403.

Next steps


®Varnish Software, Wallingatan 12, 111 60 Stockholm, Organization nr. 556805-6203