An artifact firewall is a proxy that makes a policy decision (allow,
hide, refuse) in front of every npm install or pip install, before the
package enters your software factory. A cache such as Nexus serves whatever
it is asked for; the firewall decides what has the right to come in. This
lesson is for security and platform teams who already run an artifact
manager and want to move from caching to control.
The example is Varnish Artifact Firewall, the Premium component of
Varnish Orca, in front of Nexus. Everything is checked on 1 October 2026
against Orca 0.18.1 (firewall 0.6.0): the lesson's configuration passes
the validator, but the public image carries no licence add-on
(addons=[]), so the firewall enforces nothing without a trial licence. The
lesson says so wherever it changes what you can observe.
What you will learn
- Distinguish cache, artifact firewall, SCA analysis and admission control.
- Write
allow,hideanddenyrules, and tune the quarantine of recent publications. - Set up an Orca and Nexus lab, and know what it shows without a licence.
- Block a vulnerable version and a spoofed internal name.
- Close bypasses, on CI runners as on workstations.
- Produce the evidence for SOCLE-INT-GEN-3 (in French), OSS consumption through a controlled internal proxy, without unfiltered direct pulls.
Why a cache is not enough
Your developers run dozens of npm install and pip install every day. Each
one downloads third-party code, often hundreds of transitive packages, and
runs it on workstations and CI runners. In September 2025, the
Shai-Hulud worm compromised more than 500 npm packages according to
the CISA alert of 23 September: its postinstall script collected the
tokens and credentials present in the environment, then republished itself
with the npm tokens it found.
An artifact manager such as Nexus (in French) or a cache such as Varnish Orca (in French) solves two real problems: speed and resilience against an outage of the public registry. But it serves whatever it is asked for. If a developer installs a vulnerable version, a package with a name spoofed through dependency confusion, or a version published two hours ago by a compromised account, the cache downloads it, stores it and redistributes it to the whole team.
What is an artifact firewall?
An artifact firewall sits between your clients (npm, pip, CI runners) and your repositories, and evaluates every resolution against a set of rules. At Varnish, a rule leads to one of three actions, and a global quarantine comes on top, according to the firewall configuration documentation:
| Decision | Effect | Use case |
|---|---|---|
allow | Serves the artifact | Explicit exception, trusted package |
hide | Removes the version from "latest version" resolution | Evict a version without breaking an existing lockfile |
deny | Refuses with a 403 response | Forbidden package, vulnerable version |
quarantine (default_quarantine_days) | Blocks any version published less than N days ago, whatever the rules | Safety window on fresh publications |
Quarantine is therefore not a rule action: it is a global setting, which directly answers the framework's cooldown before adoption requirement, SOCLE-INT-OSS-1 (in French). A booby-trapped version published this morning, like those of Shai-Hulud, is held back while the community spots it.
The benefit over a scanner is the moment of the decision: before ingestion. A refused dependency never enters the cache or the build. Where a scanned SBOM tells you after the fact "you installed a vulnerable component", the firewall stops the component from arriving.
Architecture of the study lab
The lab chains three components. The firewall is transparent: clients
target Orca's virtual registry, and enable_firewall: true routes that
registry through the firewall internally. The diagram follows a resolution:
Nexus's role is to centralise: a proxy repository to the public registry, a hosted repository for your internal packages, and a group that exposes a single URL. The order of the group members (hosted before proxy) already forms a first barrier against dependency confusion; the firewall adds the per-rule decision on top.
In front of Nexus, but also in front of Pulp
The firewall is not tied to Nexus. The Varnish documentation describes Orca as
an agnostic virtual registry manager: each virtual registry has a
remotes.url backend, and Orca forwards the client path as is to that
backend. Nothing therefore prevents pointing that backend at another manager,
such as Pulp (in French), the open
source artifact manager.
This setup with Pulp was checked in a lab prior to this lesson: the virtual registry loads, the PyPI index and the download of artifacts go through Orca. Two points of attention came out of it:
- The most direct setup targets the container registry: Pulp exposes its
OCI API on
/v2/at the root, like the public registry Orca already knows how to cache. - For PyPI, Pulp emits absolute download links through its
CONTENT_ORIGINsetting; Orca rewraps them in its/redirectzendpoint, andCONTENT_ORIGINmust match the URL exposed by Orca.
Putting Orca in front of Pulp is interesting because Pulp has no native quarantine: the firewall brings the missing decision layer, on entirely open source storage.
Setting up the lab
The lab fits in a docker-compose.yml (Nexus and Orca) and a few
configuration files. Images are pinned by digest; versions are those of 1
October 2026.
-
Declare Nexus and Orca
docker-compose.yml services:nexus:image: sonatype/nexus3:3.96.4@sha256:5f48f9085096e96f4111ec07d79d57ca5a4a1da5a2003a3028a0b60c62bea7d1ports: ["8081:8081"]volumes: ["nexus-data:/nexus-data"]orca:image: varnish/orca:0.18.1@sha256:44df08bdb5843ef1173bda327adb487c2bd101681b24559482c10bf6d8f1429dplatform: linux/amd64 # Orca is distributed for linux/amd64depends_on: [nexus]ports:- "8080:80" # virtual registries (client endpoint)- "9090:9090" # firewall metrics (Premium)volumes:- ./orca/orca.yaml:/etc/varnish-supervisor/default.yaml:ro- ./orca/rulesets:/etc/orca/rulesets:rovolumes:nexus-data: -
Create the Nexus repositories (proxy, hosted, group) for npm and PyPI through the REST API. The
npm-allgroup aggregatesnpm-private-hostedthennpm-public-proxy, and that is what the firewall will see.Nexus Community Edition serves no artifact until its EULA is accepted. Measured on 3.96.4: a download returns
403 You must accept the End User License Agreement (EULA), then200after acceptance through the API, which expects the body returned byGET, with"accepted": true.Terminal window curl -s -u "admin:$NEXUS_PASSWORD" http://localhost:8081/service/rest/v1/system/eula \| jq '.accepted = true' \| curl -s -u "admin:$NEXUS_PASSWORD" -H 'Content-Type: application/json' \-X POST http://localhost:8081/service/rest/v1/system/eula -d @-# 204 response, no body -
Configure the Orca firewall
orca/orca.yaml virtual_registry:registries:- name: npm-nexusdefault: trueenable_firewall: trueremotes:- url: http://nexus:8081/repository/npm-all/- name: pypi-nexusenable_firewall: trueremotes:- url: http://nexus:8081/repository/pypi-all/firewall:default_action: allow # observe in a POC; deny in productiondefault_quarantine_days: 2severity_deny_threshold: 9severity_allow_threshold: 4audit_log_output: stdoutmetrics_address: ":9090"rulesets:- path: /etc/orca/rulesets/npm-rules.yaml- path: /etc/orca/rulesets/pypi-rules.yamlThe binary embeds a validator. Measured on Orca 0.18.1, it answers
configuration is validfor this file, and refuses an unknown key (field champ_inconnu not found in type firewall.FirewallConfig) or arulesetsentry written as a plain string instead of apath:object.Terminal window docker run --rm --platform linux/amd64 \-v "$PWD/orca/orca.yaml":/c.yaml:ro -v "$PWD/orca/rulesets":/etc/orca/rulesets:ro \varnish/orca:0.18.1@sha256:44df08bdb5843ef1173bda327adb487c2bd101681b24559482c10bf6d8f1429d \varnish-supervisor -validate -config /c.yaml# configuration is valid -
Point the clients at the firewall
Orca forwards the client path as is: include the Nexus group path.
.npmrc registry=http://npm-nexus.localhost:8080/repository/npm-all/pip.conf [global]index-url = http://pypi-nexus.localhost:8080/repository/pypi-all/simpletrusted-host = pypi-nexus.localhost
Writing rules
A rule describes what to recognise (match) and what to decide
(action). Rules are grouped in a file identified by an id, and sorted by
priority. This demonstration npm rule set covers the three actions.
id: npm-demo-rulesrules: # Hard block of a specific package. - id: block-left-pad priority: 100 action: deny severity: 9.5 reason: "Package bloqué pour la démonstration." match: - type: npm name: left-pad
# Anti dependency confusion: an internal scope never comes from the public registry. - id: block-internal-scope priority: 90 action: deny reason: "Le scope @company ne doit pas être résolu depuis le registre public." match: - type: npm name: "@company/*"
# Hide old versions without breaking an existing lockfile. - id: hide-lodash-old priority: 50 action: hide reason: "Les versions de lodash < 4.17.21 ne sont pas proposées." match: - type: npm name: lodash version: "vers:npm/<4.17.21"On the Python side, the same grammar blocks a vulnerable version. The
version range uses the VERS notation (vers:pypi/<2.32.0), a standardised
format for version ranges per ecosystem.
id: pypi-demo-rulesrules: - id: block-old-requests priority: 100 action: deny severity: 9.0 reason: "Les versions de requests < 2.32.0 sont interdites." match: - type: pypi name: requests version: "vers:pypi/<2.32.0"How severity interacts with the thresholds
A rule can carry a severity score, which overrides its action. According
to the Varnish documentation, a rule whose severity is at or above
severity_deny_threshold (9 by default) leads to deny, a rule at or
below severity_allow_threshold (4 by default) leads to allow, and in
between, the rule's action applies. In the npm example, block-left-pad
(9.5) would be refused even if its action were hide.
This mechanism lets you plug in community rules, for instance the rule
sets derived from OSV data, and steer their effect with two thresholds,
without rewriting each rule. It also carries a trap: a deny rule of severity
3 is allowed.
Dynamic rules from a git repository
Loading rules from local files (path:) requires restarting Orca to take
them into account. Dynamic mode goes through a git source, which the
firewall refreshes at a regular interval, without a restart. The following
excerpt passes the 0.18.1 validator.
firewall: mode: report # decisions logged, nothing is blocked rulesets: - git: name: npm-osv-rules url: https://github.com/varnish/osv-rules.git sub_path: rulesets/npm/all.yaml ref: main # followed branch: rules change without local review interval: 30m # refresh frequency webhook: url: http://collector.interne/reloaded # notified after each reloadThe varnish/osv-rules repository exists and publishes
rulesets/npm/all.yaml. Following its main branch is a deliberate
choice: rules update on their own, at the price of trusting the publisher.
For your own rules, an internal repository, versioned and reviewed through
code review, gives a real GitOps approach. report mode is the
right starting point: every decision is brought down to allow, but stays
written in the audit log, which lets you measure the effect of a policy
before enabling it.
Studying the firewall in practice
With a trial licence mounted, the firewall enforces decisions. The most telling study scenario compares two installations. The following steps were not replayed for this lesson, for lack of a licence: they describe the documented behaviour.
-
Install an allowed dependency
Terminal window npm install is-number # no rule matches: allow -
Try a blocked dependency
Terminal window npm install left-pad # block-left-pad rule: deny, 403 response -
Read the audit log
Terminal window docker compose logs orca | grep -i denyThe log carries the decision, the package and the rule's reason (
reason). That is what you will route to a SIEM in production. -
Change a rule and replay
Remove
block-left-pad, restart Orca, run the installation again: it goes through. The policy drives the decision.
What the firewall does not protect
An artifact firewall closes one door: the ungoverned intake of dependencies. Presenting it as global protection would be a mistake, and three limits show it.
- It does not read the code of an allowed package: a dependency that is sound today can be compromised by an update, hence the quarantine.
- It protects neither a compromised CI runner, nor secret exfiltration, nor an image already built and unsigned.
- It produces neither an SBOM nor provenance: those are other components of this module.
Above all, the registry configuration encourages going through the
firewall but does not enforce it: a developer or a job can always
write npm config set registry https://registry.npmjs.org. Closing this
bypass is a topic in its own right.
Preventing bypass (CI and developer workstations)
A firewall only protects what goes through it. As long as clients can
reach registry.npmjs.org or pypi.org directly, anyone can short-circuit it
with a single command. The rule fits in one sentence: configuration
encourages, the network enforces.
On CI runners: lock down egress
On the CI side, the bypass closes cleanly because you control the
environment. Put the runners in a network with no outbound route to the
Internet: only the artifact firewall is reachable. An attempt to run
npm config set registry https://registry.npmjs.org then fails for lack of a
route. On Kubernetes, an egress NetworkPolicy that only allows the
firewall service has the same effect, and ephemeral runners prevent any
persistent local configuration.
On developer workstations: the hard case
The developer's workstation is harder to constrain: the developer often has
administrator rights on it, so no application configuration is
tamper-proof. Pushing an .npmrc, a pip.conf or a GOPROXY through MDM
or configuration management reduces friction, but can still be bypassed with a
--registry. The robust control is, once again, the network:
| Solution | Effect | Robustness |
|---|---|---|
| Cut direct egress to npmjs, pypi, Docker Hub | The public registry has no route any more | High |
Internal DNS: registry.npmjs.org resolved to the firewall | Even a hard-coded public URL lands on the firewall | High, with egress closed |
| Workstation without a direct Internet route | Everything goes through the internal proxy | Maximum |
| Remote development environment (dev container, VDI) | You control the environment's network | High |
Managed configuration (.npmrc, pip.conf, GOPROXY) | Steers the default | Low on its own |
No single row is enough on a machine where the developer is an administrator. The combination that holds: no direct route to public registries, plus a DNS that brings public names back to the firewall. Managed configuration adds comfort, not security.
Catching and detecting
The network closes the door; detection checks that it stays closed. Add a
CI check on lockfiles: a resolved entry (npm) or an index URL pointing
anywhere other than the firewall fails the build.
OSV-Scanner (in French) spots on
its side the poisoned dependencies that bypassed the firewall, and the
network proxy logs flag any attempt at direct access to public registries.
These bypasses extend the techniques described in
Attacks through package managers
(in French).
Where should it sit in your chain?
The firewall is not a substitute for the module's other controls: it holds the furthest upstream position. A mature chain combines them, each answering a different question.
| Step | Component | Role |
|---|---|---|
| Dependency intake | Varnish Artifact Firewall | Decides before ingestion |
| Centralisation | Nexus (in French) | Caches and exposes artifacts |
| Inventory | SBOM | Lists the shipped components |
| Exploitability | VEX | Documents what is really exploitable |
| Signing | Cosign | Signs and verifies the image |
| Provenance | SLSA | Attests the origin of the build |
The firewall reduces the noise downstream: fewer dubious components come in, fewer vulnerabilities to sort afterwards. It does not replace defence in depth, it starts it.
Key points
- A cache speeds things up and withstands outages; an artifact firewall decides what has the right to come in.
- Three rule actions,
allow,hideanddeny, plus a global quarantine (default_quarantine_days) on recent publications. - Severity overrides the action: above the deny threshold,
deny; below the allow threshold,allow. reportmode logs decisions without blocking anything: the right start to measure a policy.-validatechecks the configuration's structure, not the content of the rules.- Without a network egress rule, the registry configuration can be bypassed.
- At Varnish, the firewall is Premium: the public 0.18.1 image starts with
addons=[]and filters nothing. - The evidence expected by SOCLE-INT-GEN-3 is the internal proxy configuration and proof of the absence of unfiltered direct pulls.
Knowledge check
Check that the essentials of this lesson are understood. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Responding to a supply chain incident: scope, containment, notification
A new full quiz with random questions
Work again only on the questions you missed