Block a Package Version Downloaded Through JFrog Artifactory

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. The firewall sits between the clients and Artifactory: 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

  • A license that includes the Artifact Firewall. 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 served from the cache, or fetched from Artifactory.

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 diagram shows both requests for the rule this tutorial writes. The Virtual Registry receives the client’s requests and caches what is allowed, and the Artifact Firewall takes the decisions.

Sequence diagram: the npm client asks the Virtual Registry for the is-number package document, which the Artifact Firewall fetches from JFrog Artifactory, flags 7.0.0 in, and moves latest to 6.0.0. For each tarball the Virtual Registry asks the firewall first: 7.0.0 is answered with a 403 naming the rule and its reason, without contacting Artifactory, and 6.0.0 is served from the cache or fetched from Artifactory.

Step 1: Point the firewall at Artifactory

Create example-config.yaml:

varnish:
  http:
    - port: 80

virtual_registry:
  registries:
    - name: artifactory
      default: true
      base_url: http://localhost
      enable_firewall: true
      remotes:
        - url: https://example.jfrog.io

firewall:
  address: localhost
  default_action: allow
  audit_log_output: stdout
  rulesets:
    - path: /rulesets

license:
  file: /app/license.lic

The remote 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: the npm registry URL becomes http://localhost/artifactory/api/npm/npm-remote/.

base_url is the address clients use to reach the firewall. The firewall writes it into the tarball links of every package document, so the downloads come back through the firewall instead of going straight to Artifactory.

Step 2: Write a rule that holds one version

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

# 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. The reason is returned to the developer with the 403, so write it for them. See the ruleset reference for the other ways to match packages.

Start the container:

docker run --rm -p 80:80 --name firewall \
  -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

The log confirms the rule loaded:

time=... level=INFO msg="license verified" addons="[... sup-firewall ...]" expires=...
time=... level=INFO msg="loaded rulesets" default_action=allow rulesets=1 rules=1 exact_selectors=1 pattern_selectors=0
time=... level=INFO msg="Varnish is ready to receive traffic"

Step 3: Install the package the ways developers do

In another terminal, point npm at the firewall:

export NPM_CONFIG_REGISTRY=http://localhost/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/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 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/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"): Held for compatibility review
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, the ruleset and your reason. 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/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"): Held for compatibility review

Download the tarball directly

curl -s -w '\nHTTP %{http_code}\n' \
  http://localhost/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\"): Held for compatibility review","action":"deny","rule_id":"hold-is-number-7","ruleset_id":"lab-policy","reason":"Held for compatibility review"}

HTTP 403

This stays a 403 when Artifactory already holds the tarball in its cache, and when the firewall’s own cache holds an allowed version: every tarball request is evaluated, whether it is served from a cache or not.

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":"eval","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":"eval","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 eval line is one download, 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.

The audit log names the rule, not its 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