Block a Package Version in Artifactory with a Standalone Artifact Firewall

You can keep JFrog Artifactory as your repository manager and let the Artifact Firewall decide which package versions your developers and CI runners can download, with nothing else to deploy. The firewall runs on its own, from the varnish/artifact-firewall image: clients point at the firewall, the firewall forwards to Artifactory, and Artifactory keeps proxying the public registry as before. Nothing changes in Artifactory.

This tutorial blocks one version of an npm package, is-number@7.0.0, and shows what happens for each way a developer can ask for it:

  1. Point the firewall at Artifactory.
  2. Write a rule that holds one version.
  3. Install the package the ways developers do.
  4. Trace the decision in the audit log.

Prerequisites

  • An Artifact Firewall license. Request a trial license if you do not have one.
  • Docker and npm on the machine you test from.
  • An Artifactory npm remote repository that proxies https://registry.npmjs.org. This tutorial uses one called npm-remote on https://example.jfrog.io, which Artifactory serves under /artifactory/api/npm/npm-remote/. Replace both with your own.
  • Read access to that repository without credentials, for this test. Use a test instance or a test repository rather than the one your builds use.

How a request flows

Every npm install makes two kinds of request, and the firewall checks both:

  • The package document (GET /<name>) lists every version and says which one is latest. The firewall fetches it from Artifactory, marks the blocked versions, moves latest to the highest version that is allowed, and rewrites every tarball link so it points at the firewall instead of Artifactory.
  • The tarball (GET /<name>/-/<name>-<version>.tgz) is the download itself. The firewall evaluates the version before the request goes anywhere. A blocked version is answered with a 403 and the request never reaches Artifactory. An allowed one is fetched from Artifactory and streamed to the client.

Because the decision is taken before Artifactory sees the request, it does not matter whether Artifactory already holds a copy of the blocked version. The firewall keeps no copy of what it serves: every allowed download is fetched from Artifactory.

Step 1: Point the firewall at Artifactory

Create a folder called config/. The firewall reads everything from it: its configuration, your license and your rules.

Create config/default.yaml:

firewall:
  registries:
    - name: artifactory
      remote_url: https://example.jfrog.io
      routes:
        - host: localhost
  default_action: allow
  audit_log_output: stdout
  rulesets:
    - path: /etc/varnish-artifact-firewall/rulesets

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

The remote_url is the root of the Artifactory instance, not a single repository, so one registry entry serves every repository on it. Clients keep the Artifactory repository path, and the firewall recognizes it: the npm registry URL becomes http://localhost:6089/artifactory/api/npm/npm-remote/.

The route sends every request whose Host is localhost to this registry. In production, route the hostname your clients use for the firewall, such as npm-firewall.example.com, and set base_url to its URL if a proxy in front of the firewall terminates TLS. The firewall writes that address into the tarball links of every package document, so the downloads come back through it instead of going straight to Artifactory.

Copy your license to config/license.lic.

Step 2: Write a rule that holds one version

Create config/rulesets/lab.yaml:

id: lab-policy

rules:
  - id: hold-is-number-7
    action: deny
    reason: "Held for compatibility review"
    match:
      - purl: "pkg:npm/is-number@7.0.0"

The purl names one exact version. 7.0.0 is the current latest of is-number, which shows what happens to developers who do not pin a version. See the ruleset reference for the other ways to match packages.

Your folder now looks like this:

config/
├── default.yaml
├── license.lic
└── rulesets/
    └── lab.yaml

Start the firewall, with the folder mounted in place of the image’s own:

docker run --rm -p 6089:6089 --name firewall \
  -v $(pwd)/config:/etc/varnish-artifact-firewall:ro \
  varnish/artifact-firewall:0.9.0

The log confirms the registry and the rule loaded:

time=... level=INFO msg="starting Artifact Firewall" version=0.9.0 ... deployment=standalone mode=normal
time=... level=INFO msg="effective configuration" section=firewall.registries[0] name=artifactory remote_url=https://example.jfrog.io routes[0].host=localhost
time=... level=INFO msg="loaded rulesets" default_action=allow rulesets=1 rules=1 exact_selectors=1 pattern_selectors=0
time=... level=INFO msg="license verified" company=... expires=...
time=... level=INFO msg="serving proxy" address=[::]:6089 deployment=standalone

Step 3: Install the package the ways developers do

In another terminal, point npm at the firewall:

export NPM_CONFIG_REGISTRY=http://localhost:6089/artifactory/api/npm/npm-remote/

Each command below uses a fresh npm cache (--cache=$(mktemp -d)), so npm has to ask the registry instead of reusing what it fetched before.

Check the package document

npm view is-number dist-tags.latest dist.tarball --prefer-online --cache=$(mktemp -d)
dist-tags.latest = '6.0.0'
dist.tarball = 'http://localhost:6089/artifactory/api/npm/npm-remote/is-number/-/is-number-6.0.0.tgz'

Two things to check here. latest is now 6.0.0, since 7.0.0 is blocked. And the tarball link names the firewall, not Artifactory. If the link names your Artifactory host, the downloads bypass the firewall: check the route and base_url.

Install without a version

npm pack is-number --cache=$(mktemp -d)
is-number-6.0.0.tgz

The developer gets 6.0.0 and no error. Anyone who installs the package without pinning a version, or with a range that 6.0.0 satisfies, gets the highest allowed version instead.

Install the blocked version

npm pack is-number@7.0.0 --cache=$(mktemp -d)
npm error code E403
npm error 403 403 Forbidden - GET http://localhost:6089/artifactory/api/npm/npm-remote/is-number/-/is-number-7.0.0.tgz - package blocked by firewall (rule "hold-is-number-7", ruleset "lab-policy")
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 developer sees the rule and the ruleset that blocked the download. The same happens for a project whose lockfile pins is-number@7.0.0, which is what a CI runner running npm ci hits:

npm error code E403
npm error 403 403 Forbidden - GET http://localhost:6089/artifactory/api/npm/npm-remote/is-number/-/is-number-7.0.0.tgz - package blocked by firewall (rule "hold-is-number-7", ruleset "lab-policy")

Download the tarball directly

curl -s -w '\nHTTP %{http_code}\n' \
  http://localhost:6089/artifactory/api/npm/npm-remote/is-number/-/is-number-7.0.0.tgz
{"error":"package blocked by firewall (rule \"hold-is-number-7\", ruleset \"lab-policy\")"}

HTTP 403

This stays a 403 when Artifactory already holds the tarball in its cache: every tarball request is evaluated before it reaches Artifactory.

Step 4: Trace the decision in the audit log

With audit_log_output: stdout, the audit log is written to the container’s output as one JSON line per decision. The run above produced:

{"kind":"listing","registry":"artifactory","mode":"normal","outcome":"filtered","decisions":[{"purl":"pkg:npm/is-number@7.0.0","effect":"flagged","enforced_effect":"flagged","action":"deny","rule_id":"hold-is-number-7","ruleset_id":"lab-policy"}]}
{"kind":"download","purl":"pkg:npm/is-number@6.0.0","rule_id":"default","ruleset_id":"default","action":"allow","enforced_action":"allow","registry":"artifactory","mode":"normal","outcome":"served"}
{"kind":"download","purl":"pkg:npm/is-number@7.0.0","rule_id":"hold-is-number-7","ruleset_id":"lab-policy","action":"deny","enforced_action":"deny","registry":"artifactory","mode":"normal","outcome":"blocked"}

The listing line is the package document: 7.0.0 was flagged by hold-is-number-7. Each download line is one tarball request, with the rule that decided it and the outcome the client got. The id and ts fields are left out above. The audit log reference describes every field.

Neither the developer’s error nor the audit log carries the rule’s reason. To read the reason behind a rule_id, look the rule up in its ruleset, or through the admin API.

Next steps


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