What Écluse is
Écluse is a proxy you put in front of public package registries to protect the builds that install from them. Point your CI and developer tooling at Écluse instead of at a public registry. Écluse fetches from that registry on their behalf and decides which versions a build may install. npm supports reads, mirroring, and first-party publication. PyPI supports project-index and distribution reads, but not mirroring or publication.
A new public version waits in a quarantine, seven days by default, before a build can install it.
Most malicious publishes are found and pulled within days, so the wait alone sidesteps them, with
no attempt to detect malice. A security patch overrides the quarantine as soon as the advisory
data is available: a version that a synced advisory names as the exact fix for a vulnerability
skips the wait. A fix still waits when no advisory database has synced, when the advisory data is
too old, when another advisory still affects the version, or when the advisory spells the version
differently. Pin such a fix with AllowByIdentity. Everything else is deny by default and opt-in by
name.
If you run a private registry, Écluse reads it first and passes your own packages through
untouched. Any https registry that speaks the ecosystem's protocol serves in that role. Écluse can
also mirror each admitted public version into a registry you nominate, so a mirrored version
survives a public outage or yank. You declare each registry under a tag that names the store behind
it. A mirror target under the codeArtifact tag mints its own short-lived write token, and the
other tags take a static token you supply. Écluse hosts no packages itself.
Écluse ships as one container image with four roles:
ecluse proxyserves clients and runs the mirror worker.ecluse mirrorruns the mirror worker alone, when you want to scale it apart from the proxy.ecluse pilotbuilds the advisory database from the OSV exports and the EPSS feed. The fast lane, the advisory denies, and Dredger all read it.ecluse dredgerdeletes versions your rules deny at revocation from each mount's mirror target and private cache. It is the only role that deletes, and Running the Dredger covers it.
Deploying Écluse covers all four.
How it works#
The registry roles#
Écluse sees registries by role, and each role is a URL on a mount. Each ecosystem gets its own
path prefix: /npm/ or /pypi/.
| Role | What it does | Trust | Required? |
|---|---|---|---|
| Public upstream | The registry Écluse gates | Subject to admission | Defaults to registry.npmjs.org for npm and pypi.org for PyPI |
| Private upstream | Serves your own packages first | Trusted | No. Without it, a pure public gate |
| Mirror target | Receives admitted npm versions and answers presence probes | Operator-controlled | No. Declaring it makes the npm mount a mirror |
| Publication target | Receives your publish requests | Trusted | Opt-in |
These are roles, not necessarily separate servers. The recommended topology gives each one its own store and explains what you lose when two share one.
A request, step by step#
For npm, a client requests a version listing and then a tarball. PyPI reads use project indexes and distribution files instead, with no mirror or publication step.
- The listing. Écluse fetches the private and the public registry in parallel. It trusts
every private version that meets the trusted integrity floor, gates every public version
through the policy, and serves the merged listing. A public version the policy did not admit is
absent from it, so a resolver never picks it. The served
dist-tags.latestfollows the public registry's own tag when that version is admitted. Otherwise it names the highest admitted version, stable before prerelease. A private registry's or a mirror store's ownlatesttag is never served. - The tarball. A private hit streams through unfiltered, and a private miss is gated on its public metadata. When admitted, Écluse streams the tarball from the public registry while a mirror job copies it into the mirror target in the background. Mirroring is demand-driven, so only a version a client pulls gets mirrored.
- A publish. Publishing stays off until you configure a publication target. With one, Écluse refuses any name outside the mount's first-party namespaces before it writes upstream.
Private versions do not re-enter public admission. Private metadata passes the trusted integrity floor, but a conventional private npm artifact hit bypasses that metadata path. Those reads rely on the client's integrity checks. A public/private digest conflict logs a WARNING with both digests and increments the divergence counter. Comparison uses complete digest sets for each shared filename and algorithm. Hexadecimal case does not affect agreement. Diagnostics keep the original digest spelling. The private version and its tags stay listed, and direct private artifact reads still work. The alarm remains when a public rule denies the conflicting copy. The denied public version stays excluded. Only metadata that passes identity, artifact-location, and integrity checks supplies conflict evidence. A disagreement proves neither copy correct. A damaged private copy can still be served because the private repository is trusted to hold previously admitted or first-party content. Divergence alone never authorises Dredger to delete a version. Removing an allow does not revoke a mirrored version unless a named deny that applies at revocation becomes decisive. See Revoking a mirrored version.
Listings omit files whose URLs lack a filename, name . or .., or have a backslash or control character in the filename.
The check also applies after one percent decode and refuses encoded separators or invalid UTF-8.
A release disappears when no file survives.
The mirror write republishes the version object the public registry served when the worker admitted the version, so an install from the mirror resolves the same dependencies, executable mappings, engines, scripts, and deprecation notice as an install from the public registry. The worker reads that object at admission time, never from the queued job.
- Écluse keeps every field the author wrote, including fields it does not model.
- Écluse rewrites only what it verified itself: the name, the version, the attachment, and the
distlocation and digests. A digest the public registry declared but the worker did not verify is dropped rather than republished. - Écluse strips
dist.signatures,dist.attestations, and every underscore-prefixed field, because they describe the public registry's own keys, bundles, and bookkeeping. - A version whose source object is unavailable is not mirrored: the worker publishes nothing, retires the job, and logs why, apart from a policy denial.
Écluse never edits a version the mirror already holds, so a copy that an earlier release mirrored
with the shorter manifest keeps it. To repair one, delete that version from the mirror target with
the store's own tooling (DeletePackageVersions on CodeArtifact, an unpublish on Verdaccio), then
request the version through the proxy again. The next tarball request enqueues a fresh mirror job,
which republishes the version with its full metadata. A separate private cache that retained the
old copy keeps serving it until you remove that copy too.
Each mirror write sets dist-tags.latest on the mirror target. Écluse reads the public
registry's own latest and the versions the mirror holds after the write, then names the public
target when the mirror holds it. Otherwise it names the highest stable version present, or the
highest prerelease when no stable version is present. Mirroring one version never makes that
version latest by itself, so back-filling an old version does not change what an unqualified
install resolves. Écluse owns latest on every public package it mirrors and overwrites a value
it did not set. That stored tag governs a client pointed straight at the mirror, never the listing
Écluse serves. First-party names never reach the mirror worker, and their release tags are
untouched.
A mirror job whose target metadata cannot be read is not published, because the tag cannot be chosen without the current inventory. It follows the worker's usual fault handling instead, which retries a transport failure and retires a fault no redelivery can clear. Two workers mirroring the same package at the same instant still race: an npm publish carries no compare-and-set, so the last write wins. The next job that publishes a new version of that package corrects the tag.
Before publication, the mirror worker verifies fetched bytes against the current admitted
metadata. For npm dist.integrity, any matching SRI alternative at the strongest algorithm
permits publication, regardless of token order. A matching weaker digest cannot rescue a
stronger-algorithm mismatch. This alternative rule applies only to SRI components.
The policy#
The policy is deny by default: a public version reaches a build only when a rule admits it. Rules run in precedence order and the first decisive one wins. The revoke and the install-time deny sit above every allow by default.
| Rule | Band | On by default | What it decides |
|---|---|---|---|
min-age | Allow | Yes | Admits a public version older than seven days: the quarantine |
remediation-fast-track | Allow | Yes | Admits a version a synced advisory names as the exact fix for a vulnerability, as long as no other advisory still affects it |
AllowScope | Allow | No | Allow-lists every package under a scope you name |
AllowByIdentity | Allow | No | Pins a package or a package@version by exact name |
DenyByIdentity | Deny | No | Revokes a package or version |
DenyInstallTimeExecution | Deny | npm only | deny-scripts denies npm install scripts at admission, leaving stored copies in place |
DenyIfCve | Deny | No | Denies versions with a known vulnerability above a severity you choose |
DenyIfEpss | Deny | No | Denies versions with a known vulnerability whose exploitation probability (EPSS) is at or above a threshold you choose |
remediation-fast-track abstains until a first advisory database syncs, so without one only the
quarantine supplies the shared allow. The npm mount also denies install scripts by default.
Disable deny-scripts under mounts.npm.rules, or add an AllowByIdentity pin with precedence above 300.
The off rules opt in by name, and
Rule policy has their knobs.
Independent of the rules, Écluse serves a public file only if it carries a digest that meets
the public integrity floor, sha256 by default. The check runs per file, so a release that ships
several, as a Python release ships an sdist beside its wheels, keeps the files that clear the
floor and loses the ones that do not. A release disappears only when no file of it survives. On a
custom or off-spec public upstream this has a gotcha: files without such a digest silently
disappear, and downloading one 403s. To serve such a source, give it the private upstream role
and loosen the trusted floor below sha256. The mechanics are in
Integrity floors.