Deploying Écluse

This page covers the container image, the deployment topology to aim for, and the network controls around it.

The image and its roles#

Écluse ships as one reproducible container image, a multicall executable selected by the container command:

All roles share one configuration. Multiple Pilot or Dredger instances race, duplicate API calls, and overlap registry deletions.

ecluse pilot compile --out DIR runs one OSV compilation and exits. It fetches an ecosystem's advisory export (--ecosystem, default npm, with --source URL overriding the configured advisories.osvExportBaseUrl). It writes <ecosystem>-osv-schema<N>.db (e.g. npm-osv-schema3.db) into DIR and exits non-zero on failure. --upload also publishes the artifact to the advisory bucket, a full sync cycle in one invocation, and aborts at once without a configured bucket. A corrupt or truncated export aborts the compile without publishing, so a running proxy keeps its last-good database. To avoid an idling Pilot pod, run the one-shot as a Kubernetes CronJob with concurrencyPolicy: Forbid, which keeps it a singleton. Give the pod s3:PutObject through IRSA or workload identity rather than mounted keys, and schedule it less often than the proxy polls.

Pin the image by digest and verify its provenance and SBOM attestations before you run it. The recipe is in Verifying the image.

This is the posture the threat model treats as canonical. Aim for it unless you have a specific reason to diverge.

  1. Run three registries, not one. Give the three roles distinct backends. The publication target is a first-party store, the mirror target is a public-derived store, and ECLUSE_MOUNTS__NPM__PRIVATE_UPSTREAM is a pull-through read endpoint that unions both. Separating provenance keeps the mirror auditable. The one hard rule: the aggregating endpoint must union trusted stores only, never a direct public upstream. Otherwise raw ungated packages reach clients as trusted and bypass the gate. See registry-level composition.
  2. Let callers use their own identity. The default forwards each caller's credential to the private upstream and publication target. Access then matches your registry IAM exactly, and Écluse holds no standing read credential. Nothing to set. See Credential flow and authority.
  3. Mint the mirror-write token from the container role. Point ECLUSE_MOUNTS__NPM__MIRROR_TARGET at a CodeArtifact endpoint. The worker then mints a short-lived token under the task or instance role instead of carrying a static secret. Scope that role write-only to the mirror store and keep ECLUSE_MOUNTS__NPM__MIRROR_CODE_ARTIFACT_TOKEN_DURATION short: it is Écluse's only standing credential and it writes the trusted store. Scope the mirror queue the same way. Grant only the serve role SendMessage, and only the worker ReceiveMessage/DeleteMessage/ChangeMessageVisibility. Anyone who can write the queue can force a write to the trusted store. ChangeMessageVisibility is load-bearing, not optional. The worker uses it to hold a long publish. It also backs a dead-lettered poison message off so the message rides your redrive policy to the DLQ. Without the grant an over-cap artifact silently churns on the ordinary visibility cadence instead.
  4. Let the edge own access, and leave ECLUSE_SERVER__AUTH_TOKEN off. Écluse is not your access boundary. Front it with a gateway, mesh, or IAP, and restrict reachability both north-south and east-west (pod-to-pod). An ingress-only allow-list that leaves the pod reachable inside the cluster is a common vulnerability. See Edge authentication.
  5. Fence egress, keep metadata reachable. Default-deny outbound. Allow only your upstreams, the mirror target, the metadata endpoint, and the advisory bucket when ECLUSE_ADVISORIES__BUCKET is set (the proxy needs s3:GetObject to sync it). Require IMDSv2 with hop limit 1. Do not block the metadata endpoint: Écluse needs it to mint credentials. See Network egress.
  6. Make the proxy unbypassable. Deny CI runners (and, where practical, workstations) outbound access to the public registries. See Locking down CI egress.
  7. Verify what you run. Pin the image by digest and verify its attestations (Verifying the image).

The reasoning behind each choice, and the residual risks it accepts, is in the threat model and Security posture.

What a deviation costs#

Écluse still runs if you diverge, but each deviation trades away a protection, and one is silent (Écluse cannot detect it, so nothing warns you):

The threat model records both. The other deviations self-announce. An open edge leans on your network boundary. A static publish credential fails closed at boot without that edge, and a static mirror-write secret forgoes the minted token.

Edge authentication and client credentials#

Edge authentication to the proxy has two shipped modes:

  1. Open: ECLUSE_SERVER__AUTH_TOKEN unset, so the network layer (VPC, service mesh) owns access control. Appropriate only on a closed network.

  2. Static token: ECLUSE_SERVER__AUTH_TOKEN set. Clients send it as Authorization: Bearer <token>. For an npm-protocol client that is the _authToken line, keyed by the mount's host and path:

    # .npmrc
    registry=https://ecluse.example.internal/npm/
    //ecluse.example.internal/npm/:_authToken=${ECLUSE_TOKEN}

The edge token never becomes the upstream one. Reads run passthrough: Écluse forwards the caller's own credential to the private upstream, which stays the authority on what that caller may see. It strips that credential before the anonymous public fetch, so a client token never leaves for a public registry. It never caches the private origin across callers, so one caller's read can never answer another's. By default the only credential of Écluse's own is a mirrored mount's write to the mirror target, derived from the mirror-target URL.

A publish forwards the publisher's own token the same way. Opt into a static ECLUSE_MOUNTS__NPM__PUBLICATION_TARGET_TOKEN and Écluse publishes as itself instead. That needs ECLUSE_SERVER__AUTH_TOKEN, or the boot refuses (PublishStaticCredentialNeedsEdge), because the pairing would let any unauthenticated client publish under it. ECLUSE_MOUNTS__NPM__PUBLISH_ALLOW limits which package names a client may publish. It authorises names, not callers, and it is not authentication. The reasoning is in security posture and Publishing first-party packages.

Network egress#

Écluse fetches from the registries you point it at, and some URLs it follows (a version's dist.tarball) come from upstream responses. Apply least-privilege egress in two layers. Écluse provides the first in the application, with an origin-aware trust model:

The dist.tarball host gate. Upstream chooses dist.tarball, so Écluse fetches a tarball only from the same allowlisted host that served the listing. It compares host and port as a pair. Écluse upgrades a plaintext dist.tarball to https on its own host. On any other host it drops the tarball and skips the version. There is no widening knob.

Provide the second layer at the platform, default-denying egress and allowing only your registries, mirror target, and the metadata endpoint:

Do not block the metadata endpoint or internal ranges for the proxy itself. Écluse reaches metadata through the AWS SDK to mint its instance-role credentials. Denying it breaks those credentials. IMDSv2 hop limit 1 keeps the minting working while stopping a neighbour or forwarded request from reaching metadata through extra hops. Grant the proxy only the cloud permissions it needs: the mirror-write credential and the advisory-bucket read (s3:GetObject) when ECLUSE_ADVISORIES__BUCKET is set, nothing more. The trust assumptions behind this are in Security posture.

Pilot and Dredger need distinct, tightly scoped egress:

Locking down CI egress#

The controls above secure Écluse's own egress. This one secures your consumers'. If you control CI, deny runners outbound access to the public registries (registry.npmjs.org and the equivalents for other ecosystems), and let them reach only Écluse and your internal services. A misconfigured job then fails instead of pulling an unvetted package. A stray --registry flag, a committed .npmrc, or a tool that ignores your settings cannot route around a network that only reaches Écluse. That makes the policy unbypassable rather than merely default (MOTIVATION, The bar). The same idea extends to developer workstations, a softer control than CI.