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 example-config.yaml and add a path: entry to the firewall.rulesets list:

varnish:
  http:
    - port: 80

virtual_registry:
  registries:
    - name: npmjs
      default: true
      enable_firewall: true
      remotes:
        - url: https://registry.npmjs.org

firewall:
  address: 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
        interval: 1h
    - path: /rulesets

license:
  file: /app/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, 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 (0). 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:

./
├── example-config.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 80:80 -p 6090:6090 --name orca \
  -v $(pwd)/example-config.yaml:/app/config.yaml:ro \
  -v $(pwd)/rulesets:/rulesets:ro \
  -v $(pwd)/license.lic:/app/license.lic:ro \
  varnish/orca --config /app/config.yaml

You should see the firewall load both ruleset sources at startup.

time=... level=INFO msg="License: Firewall enabled"
time=... level=INFO msg="Loading VCL Group into Varnish" name=npmjs
time=... level=INFO msg="Varnish is ready to receive traffic"
time=... level=INFO msg="Loaded Firewall ruleset" id=express-ruleset rules=1
time=... level=INFO msg="Loaded Firewall ruleset" id=osv-npm rules=218718
time=... level=INFO msg="Rulesets loaded" default_action=allow rulesets=2 rules=218719 exact_selectors=215693 pattern_selectors=0
time=... level=INFO msg="Starting Firewall" address=localhost:6090

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

This way you don’t need to append --registry=... to every npm command. The --dry-run flag means nothing actually downloads; it just resolves the request through the firewall. The --prefer-online forces npm to actually hit the registry path more reliably.

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

The reason you wrote in the ruleset is returned directly to the developer, making it clear why the package was 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 to hide newer versions of react.

Add a new ruleset file

Create rulesets/react.yaml:

# rulesets/react.yaml
id: react-ruleset

rules:
  - id: hide-newer-react
    action: hide
    priority: 10
    reason: "React versions newer than 18.0.0 are not yet approved"
    match:
      - purl: "pkg:npm/react"
        version: "vers:npm/>18.0.0"

Trigger a live reload

curl -v http://localhost:6090/api/update

By default the firewall listens on port 6090. On /api/update, it re-reads all rulesets from the configured paths (and any Git repositories), and changes to existing or new rulesets are reflected at runtime, with no downtime and no restart.

Verify the hide behaviour

Even though React’s actual latest is in the 19.x line, the firewall steers latest to the highest allowed version (18.0.0):

npm --verbose pack react --dry-run --prefer-online
# Audit-Logs: {"purl":"pkg:npm/react@18.0.0","rule_id":"default","ruleset_id":"default","action":"allow","effective_action":"allow"}

Hidden versions can still be fetched when pinned explicitly:

npm --verbose pack react@19.0.0 --dry-run --prefer-online
# Audit-Logs: {"purl":"pkg:npm/react@19.0.0","rule_id":"hide-newer-react","ruleset_id":"react-ruleset","action":"hide","effective_action":"hide"}

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 (returns 403 with your reason).

Rule actions at a glance

Actionnpm install pkg (latest)npm install pkg@x.y.z (exact)
allowVersion is served normally and eligible for latest.Served normally.
hideExcluded from latest; the next allowed version is returned instead.Still served (use deny to block pinned fetches).
denyExcluded from latest and any fetch returns 403.Returns 403 with your reason message.

Next steps