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:
- Add a local ruleset directory to your config.
- Write your first rule.
- Test it.
- Reload rules without restarting.
Prerequisites
- You have completed the Getting Started tutorial, or have an equivalent Orca + Firewall setup running locally.
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
| 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 with your reason message. |
Next steps
- Full Ruleset YAML Schema Reference: every field documented, including severity-based rules, namespace scoping, and quarantine overrides
- Observe with Grafana how to scrape the built-in metrics endpoint to track rule hits, manifest rewrites, and request latency