verdaccio-maven-s3-proxy
v1.3.0
Published
Maven pull-through cache middleware for Verdaccio: parallel fan-out to configured repositories, dual-stream to client and S3.
Maintainers
Readme
Verdaccio Maven S3 Proxy
Verdaccio 6+ middleware: Maven pull-through cache. Gradle hits /maven2/**; plugin GETs the CDN (S3 behind CloudFront), and on miss fans out to configured upstreams, streams the client, and PUTs S3.
Origin auth is required on /maven2/** and GET /-/maven/urls. Gradle sends HTTP Basic:
username = <shared client id, e.g. maven-client>
password = <OAuth JWT issued by Verdaccio>The plugin rewrites Basic to Authorization: Bearer <JWT> so a JWT
verification middleware (if you install one) sees the same header npm sends.
Before serving, this plugin also validates the HS256 signature, exp, and
name. Failures return 401 before any CDN or upstream request.
When the cache cannot serve (no upstream reachable, upstream error,
misconfiguration, missing JWT secret), /maven2/** answers 404 by default
(errors_as_404: true). Gradle treats a repository 5xx as fatal and does not
try the next maven { } block; a 404 makes it fall through. Set
errors_as_404: false to return the real status (502, 503, 413, …). The cause
is always in the logs and the x-error header.
GET /-/maven/health stays unauthenticated for the kubelet and still returns
503 when the plugin cannot serve.
Installation
npm install verdaccio-maven-s3-proxyPoint Verdaccio plugins at the directory that contains this package (or install it next to other plugins). CommonJS build — Verdaccio require()s the plugin and calls the factory without new.
Node.js 20 or newer is required.
Local development
npm install
npm run buildplugins: /path/to/plugins
middlewares:
maven-s3-proxy:
enabled: true
# ... see ConfigurationConfiguration
Verdaccio forwards middlewares.maven-s3-proxy untouched. Schema is this plugin's.
middlewares:
maven-s3-proxy:
enabled: true
bucket: your-artifacts-bucket
region: eu-west-1
cdn_url: https://cdn.example.internal
# service credential the plugin sends on CDN GETs (not a developer JWT)
# cdn_basic_user: cdn-reader
# cdn_basic_password: "…"
never_cache:
- '**/*.module'
urls:
- https://repo.maven.apache.org/maven2
- https://dl.google.com/dl/android/maven2
# Exact off-repository hosts that a 3xx may target (object-store hand-offs).
redirect_hosts:
- storage.googleapis.com
# optional; omit to accept any Basic username (password must still be a JWT)
# basic_users:
# - maven-client
# - ci-bot
# default true: cache failures are 404 so Gradle can try the next repository
# errors_as_404: trueList maven-s3-proxy before any JWT verification middleware under
middlewares. The Basic→Bearer adapter is installed immediately; Maven routes
are registered after all middleware plugins, producing this chain:
Basic adapter → JWT verifier → Maven route
You still need a Verdaccio JWT secret (same one OAuth UI signs with):
secret: your-verdaccio-hmac-secretProduction: omit s3_endpoint, access_key_id, secret_access_key. SDK uses real AWS endpoints; credentials come from Pod Identity / IRSA. Omit cdn_url to skip the CDN and always fan out to upstreams.
CDN viewer auth: Gradle never hits the distribution. The plugin GETs cdn_url with optional cdn_basic_user / cdn_basic_password. Put that same Basic <base64(user:pass)> string in a CloudFront Function (viewer-request) allowlist. Do not forward the caller's JWT to the edge (Functions cannot verify HS256; Authorization in the cache key would split HIT-CDN per user). Keep Authorization out of the CloudFront cache policy. A 401 from the CDN is logged and the plugin falls through to upstreams.
Options
| Name | Type | Description |
| --- | --- | --- |
| enabled | boolean | Default true. false makes /maven2/** unusable (404 if errors_as_404, else 503). |
| bucket | string | Required. S3 bucket for cached artifacts. |
| urls | string[] | Upstream Maven repos. Probed in parallel on a miss; the first configured repo with a 200 wins and lower-priority probes are aborted. Put canonical repos first. |
| redirect_hosts | string[] | Exact off-repository hostnames that a 3xx may target. Repository hosts are trusted automatically. Other public hosts are refused unless listed. |
| never_cache | string[] | Verdaccio-style globs: stream to client, never store. SNAPSHOT paths and maven-metadata.xml* are always skipped even without a glob. |
| region | string | AWS region. Default eu-west-1. |
| cdn_url | string | CDN base URL (no trailing slash). Plugin GETs this URL; Gradle never sees it. Omit = skip CDN hop, serve from upstreams only. |
| cdn_basic_user / cdn_basic_password | string | Optional viewer Basic the plugin sends on CDN GETs. Must match the edge allowlist (CloudFront Function / nginx). Service credential, not a developer JWT. Omit = unauthenticated hop (anyone who can GET the distribution can read cached artifacts). |
| basic_users | string[] | Optional allowlist of Gradle Basic usernames. Omit = any non-empty username. The password is either an OAuth JWT (forwarded as-is) or an opaque CI key, which the adapter normalizes to Bearer base64(key) so verdaccio-static-access-token (listed before the JWT verifier) can swap it for a signed JWT. A key matching no static token is rejected 401 by the Maven handler. |
| errors_as_404 | boolean | Default true. Cache failures become 404 so Gradle falls through. false returns the real code: 503 unreachable/misconfigured, 502 upstream/CDN/internal, 413 over the size cap. |
| allow_anonymous | boolean | Default false. Dev/test only. When true, a request with no Authorization header is served without authentication; a request that carries a header still takes the normal JWT path (a bad token is still rejected). Logs a startup warning when enabled. Never enable in production. |
| s3_endpoint | string | Local harness only (e.g. MinIO). |
| access_key_id / secret_access_key | string | Local harness only. |
Routes:
GET /maven2/**— artifacts (Basic or Bearer; 401 without a valid JWT; cache failures default to 404)GET /-/maven/health— readiness (kubelet; no auth; 503 if the plugin cannot serve)GET /-/maven/urls— configured urls (same auth as artifacts)
npm routes stay untouched.
Production operations
- Grant the pod only the S3 actions this plugin uses, scoped to
maven2/*and.incoming/*in its cache bucket. - Enable bucket versioning for recovery and forensic comparison.
- Expire incomplete multipart uploads and
.incoming/staging keys with an S3 lifecycle rule. - Set CloudFront error caching minimum TTL to zero for missing-origin responses (normally 403 and 404). Otherwise a CDN miss observed just before the S3 PUT can stay cached after the object exists.
- Purging a compromised object requires both deleting its S3 key and invalidating the matching CloudFront path.
- Alert on circuit opens, CDN 401s, failed uploads, and fan-out latency.
- Use Gradle dependency verification or committed dependency checksums. Transport integrity cannot establish publisher authenticity when a configured upstream is compromised.
License
MIT.
