A Nexus-style artifact repository supporting Maven, npm, Helm, CRAN, PyPI, and OCI in hosted, proxy, and group modes. Single static Go binary; zero external dependencies for eval mode; Postgres + S3-compatible object store for production.
Eval (zero dependencies):
docker compose up # forge on :8080, data in a named volumeProduction (Kubernetes):
# Bundles Postgres + SeaweedFS (S3) — no external services needed
helm install forge-stack deploy/helm/forge-stack \
--set forge.image.tag=latest \
--waitBinary:
go build -o forge ./cmd/forge
./forge -addr :8080 -data ./data| Format | Hosted | Proxy | Group | Clients verified |
|---|---|---|---|---|
| Maven | ✅ | ✅ | ✅ | mvn 3.9, gradle 8.7 |
| npm | ✅ | ✅ | ✅ | npm, pnpm, yarn |
| Helm | ✅ | — | ✅ | helm 3.x (repo + oci://) |
| CRAN | ✅ | ✅ | ✅ | R install.packages, renv, pak |
| PyPI | ✅ | ✅ | ✅ | pip 24.x, twine 5.x |
| OCI | ✅ | ✅ | ✅ | oras, crane, helm push oci:// |
All clients are exercised by the conformance suite against a live forge instance
(see internal/conformance/). The suite runs in CI on every push.
# npm — install through the proxy
npm install lodash --registry http://localhost:8080/repository/npm-proxy/
# npm — publish to hosted
npm publish --registry http://localhost:8080/repository/npm-hosted/
# Maven — resolve through the proxy (settings.xml)
# <repository><url>http://localhost:8080/repository/maven-central/</url></repository>
# Maven — deploy to hosted (settings.xml + distributionManagement)
mvn deploy -DrepositoryId=forge -Durl=http://localhost:8080/repository/maven-hosted/
# Helm — classic repo
helm repo add forge http://localhost:8080/repository/helm-hosted/
helm push mychart-0.1.0.tgz oci://localhost:8080/docker-hosted # OCI mode
# CRAN (R)
install.packages("pkg", repos="http://localhost:8080/repository/cran-hosted/")
# or set as your default mirror:
options(repos=c(forge="http://localhost:8080/repository/cran-public/"))
# PyPI (Python)
twine upload --repository-url http://localhost:8080/repository/pypi-hosted/ dist/*
pip install --index-url http://localhost:8080/repository/pypi-hosted/simple/ mypkg
# or through the group (internal packages shadow upstream ones of the same name):
pip install --index-url http://localhost:8080/repository/pypi-public/simple/ six
# OCI / Docker — a proxy caches upstream images (Docker Hub, ghcr.io, quay.io)
docker pull localhost:8080/repository/docker-public/library/alpine:latest
# OCI / Docker
oras push localhost:8080/docker-hosted/myimage:v1 artifact.binEnable token auth with -auth:
./forge -addr :8080 -data ./data -auth
# Prints a bootstrap admin token on first run. Store it — shown once.Create scoped tokens via the API:
curl -s -X POST http://localhost:8080/api/v1/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description":"ci-bot","grants":[{"repo":"npm-hosted","actions":["read","write"]}]}'Grants carry explicit actions — read, write, delete, admin — per
repository ("*" = all), optionally narrowed by content selectors
("selectors":["com/acme/**"]); admin on * is the global administrator,
admin on one repo delegates managing just that repo (see
docs/auth.md). Repos support anonymousRead: true for open
access (typical for install/resolve paths). Token auth is enforced as middleware
before every handler.
Forge authenticates operators against any OIDC identity provider — Keycloak, Entra / Azure AD, Okta, ADFS. Active Directory integrates either through the IdP (Keycloak federates on-prem AD over LDAP; Entra ID fronts Azure AD directly) or via a direct LDAP/AD bind — see Direct LDAP / Active Directory login below. Both paths share the same group→role mapping.
Setting the issuer enables SSO. Every flag has a matching OIDC_* env var (the
flag overrides the env); prefer the env var for the client secret, since flags are
visible in ps:
./forge -addr :8080 -data ./data -auth \
-oidc-issuer https://keycloak.example.com/realms/forge \
-oidc-client-id forge \
-oidc-client-secret "$OIDC_CLIENT_SECRET" \
-oidc-redirect-url https://forge.example.com/auth/oidc/callback \
-oidc-group-mappings 'forge-admins:admin,developers:write,staff:read'| Flag / env | Purpose |
|---|---|
-oidc-issuer / OIDC_ISSUER |
IdP issuer URL; set this to enable SSO |
-oidc-client-id / OIDC_CLIENT_ID |
OAuth client ID |
-oidc-client-secret / OIDC_CLIENT_SECRET |
OAuth client secret (prefer the env var) |
-oidc-redirect-url / OIDC_REDIRECT_URL |
https://<host>/auth/oidc/callback |
-oidc-groups-claim / OIDC_GROUPS_CLAIM |
ID-token claim with group membership (default groups) |
-oidc-group-mappings / OIDC_GROUP_MAPPINGS |
group:role,… mapping IdP groups onto roles |
OIDC_DEFAULT_GRANTS |
JSON grants applied when no group matches (default: read on *) |
-oidc-token-ttl / OIDC_TOKEN_TTL |
SSO session lifetime (default 8h) |
Group → role mapping is the centerpiece: each IdP group maps to a base role
(admin/write/read); the highest matching role wins, and a login with no
matching group falls back to OIDC_DEFAULT_GRANTS. On first login an SSO user is
provisioned into the Users tab (no local password); disabling that user there
blocks future SSO logins. The live config and mapping table are shown read-only on
the Access admin page.
docker run -p 8081:8080 \
-e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-devIn the Keycloak admin console: create a realm (forge); create a confidential
client (forge) with redirect URI http://localhost:8080/auth/oidc/callback and
copy its secret; create a group forge-admins and a user in it; add a
Group Membership client mapper named groups (token claim name groups,
"Full group path" off) so the ID token carries the group. Then start forge with
-oidc-issuer http://localhost:8081/realms/forge and
-oidc-group-mappings forge-admins:admin, and sign in via "Sign in with SSO".
Users sign in on the normal login form with their directory username and
password. Forge verifies the credentials against the directory by search-then-bind
(bind as a service account → find the user → re-bind as that user) and, on success,
mints a normal forge token — the AD password never leaves the login POST and is
never written into .npmrc, settings.xml, or CI config. Group membership feeds the
same group→role mapping as OIDC. Anonymous-read repositories need no login at all.
Setting -ldap-url enables it. Every flag has a matching LDAP_* env var (prefer
the env var for the bind password, which is visible in ps as a flag):
./forge -addr :8080 -data ./data -auth \
-ldap-url ldaps://dc1.example.com:636,ldaps://dc2.example.com:636 \
-ldap-bind-dn 'cn=forge-svc,ou=service,dc=example,dc=com' \
-ldap-bind-password "$LDAP_BIND_PASSWORD" \
-ldap-user-base-dn 'ou=people,dc=example,dc=com' \
-ldap-user-filter '(sAMAccountName=%s)' \
-ldap-group-mappings 'forge-admins:admin,forge-devs:write,staff:read'| Flag / env | Purpose |
|---|---|
-ldap-url / LDAP_URL |
Server URL(s), comma-separated for failover; set this to enable LDAP login |
-ldap-start-tls / LDAP_START_TLS |
Issue StartTLS on ldap:// connections before binding |
-ldap-ca-cert / LDAP_CA_CERT |
PEM CA bundle for the TLS connection (default: system roots) |
-ldap-bind-dn / LDAP_BIND_DN |
Service-account DN for the search step (empty = anonymous search) |
-ldap-bind-password / LDAP_BIND_PASSWORD |
Service-account password (prefer the env var) |
-ldap-user-base-dn / LDAP_USER_BASE_DN |
Base DN for the user search |
-ldap-user-filter / LDAP_USER_FILTER |
User filter; %s = escaped login name (default (uid=%s); AD: (sAMAccountName=%s)) |
-ldap-email-attr / LDAP_EMAIL_ATTR |
Attribute holding the user's email (default mail) |
-ldap-group-mode / LDAP_GROUP_MODE |
memberof (read the attribute off the user — AD default) or search |
-ldap-group-base-dn / LDAP_GROUP_BASE_DN |
Base DN for group search (mode search) |
-ldap-group-filter / LDAP_GROUP_FILTER |
Group filter; %s = escaped user DN (e.g. (&(objectClass=groupOfNames)(member=%s))) |
-ldap-group-attr / LDAP_GROUP_ATTR |
Attribute holding the group name (default cn) |
-ldap-group-mappings / LDAP_GROUP_MAPPINGS |
group:role,… mapping directory groups onto roles |
LDAP_DEFAULT_GRANTS |
JSON grants applied when no group matches (default: read on *) |
-ldap-token-ttl / LDAP_TOKEN_TTL |
LDAP session lifetime (default 8h) |
The connection is TLS-protected: use ldaps://, or ldap:// with -ldap-start-tls.
-ldap-insecure-skip-verify disables certificate verification for dev only and logs a
loud warning. When -config mode declares an ldap block, that block wins over these
flags. Local users (if any) are tried before LDAP, so the bootstrap admin keeps working.
The live config and mapping table are shown read-only on the Access admin page.
docker run -p 389:1389 \
-e LDAP_ADMIN_USERNAME=admin -e LDAP_ADMIN_PASSWORD=adminpassword \
-e LDAP_USERS=alice,bob -e LDAP_PASSWORDS=alicepw,bobpw \
-e LDAP_ROOT=dc=example,dc=org \
-e LDAP_ADMIN_DN=cn=admin,dc=example,dc=org \
bitnami/openldap:latestBitnami seeds users under ou=users,dc=example,dc=org and a group readers
(ou=users, groupOfNames) containing them. Start forge with:
./forge -auth \
-ldap-url ldap://localhost:389 \
-ldap-bind-dn 'cn=admin,dc=example,dc=org' \
-ldap-bind-password adminpassword \
-ldap-user-base-dn 'ou=users,dc=example,dc=org' \
-ldap-user-filter '(cn=%s)' \
-ldap-group-mappings 'readers:write'Then sign in on the login form as alice / alicepw. A repeatable end-to-end
validation harness (custom LDIF for forge-admins/forge-devs, curl-driven login
assertions) lives at scripts/ldap-validate.sh.
Register HTTP endpoints (Admin → Webhooks, or POST /api/v1/webhooks) to receive
events: artifact.published, artifact.deleted, artifact.cached (proxy cache
fill), and cleanup.completed. Delivery is durable (Postgres queue in production,
in-memory in eval) with bounded exponential-backoff retries that also honour a
Retry-After header on 429/503.
Each delivery POSTs a JSON envelope and these headers:
| Header | Meaning |
|---|---|
X-Forge-Event |
event type, for routing without parsing the body |
X-Forge-Delivery |
stable delivery id — identical across retries; dedup on it |
X-Forge-Timestamp |
Unix seconds, signed alongside the body |
X-Forge-Signature |
sha256=<hex> HMAC-SHA256 over "{timestamp}.{body}" |
Verify a delivery by recomputing the signature over the raw request body and rejecting timestamps outside a tolerance window (replay protection):
import hmac, hashlib, time
def verify(secret, headers, raw_body, tolerance=300):
ts = int(headers["X-Forge-Timestamp"])
if abs(time.time() - ts) > tolerance: # replay window
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256)
return hmac.compare_digest("sha256=" + mac.hexdigest(), headers["X-Forge-Signature"])The body is a flat envelope: {"schemaVersion":2,"id":"<delivery-id>","type":..., "repo":...,"format":...,"path":...,"timestamp":...,"data":{...}}.
Target URLs are checked against an SSRF policy at registration and at dial time
(defeating DNS rebinding): loopback, link-local, private, and the
169.254.169.254 cloud-metadata endpoint are refused. Set
WEBHOOK_ALLOW_PRIVATE=1 for internal-only deployments where receivers live on a
private network. Recent delivery attempts (including dropped/dead-letter) are
visible per endpoint in the UI and at GET /api/v1/webhooks/{id}/deliveries;
forge_webhook_deliveries_total{result} counts outcomes for alerting.
One shared spine; formats are plugins.
HTTP /repository/{repo-name}/{path...}
│
server.go resolves repo name → Repository
│ looks up Format → Handler
▼
format.Registry maps "maven"/"npm"/"helm"/"cran"/"pypi"/"oci" → Handler
│
Handler.Serve() receives format.Context (repo, blob, meta, http client, sub-path)
│
┌──────┴──────┐
blob.Store meta.Store interfaces; FS impl for eval, S3+Postgres for production
Adding a format = implementing one interface:
type Handler interface {
Format() string
Serve(w http.ResponseWriter, r *http.Request, c *Context)
}Nothing in routing, storage, or the repository model knows what Maven is.
Storage backends:
| Eval | Production | |
|---|---|---|
| Blob | filesystem (data/blobs/) |
any S3-compatible store (set S3_ENDPOINT) |
| Meta | filesystem (data/meta/) |
Postgres (set POSTGRES_DSN) |
| Queue | in-memory | Postgres (auto-selected when POSTGRES_DSN is set) |
The deploy/helm/forge chart is the primary install path.
# Standalone chart — point at existing Postgres + S3
helm install forge deploy/helm/forge \
--set extraEnv.POSTGRES_DSN="postgres://..." \
--set extraEnv.S3_ENDPOINT="https://..." \
--set extraEnv.S3_BUCKET="forge-artifacts"
# forge-stack — bundles Postgres + SeaweedFS (S3), no external deps
helm install forge-stack deploy/helm/forge-stackThe chart includes: liveness/readiness/startup probes, HPA, PodDisruptionBudget,
graceful SIGTERM drain, non-root + read-only root FS, multi-arch image
(amd64/arm64), ConfigMap/Secret-based config, Prometheus ServiceMonitor.
GitOps examples: deploy/gitops/argocd-application.yaml and
deploy/gitops/flux-helmrelease.yaml.
cmd/forge/ entrypoint — wires stores, registers handlers, seeds repos
internal/
blob/ blob.Store interface; FS + S3 implementations + contract suite
meta/ meta.Store interface; FS + Postgres implementations + contract suite
auth/ token store, per-repo RBAC, auth middleware (Bearer + npm Basic)
proxy/ shared proxy fetcher: TTL, ETag revalidation, negative cache,
stale-on-error, retries, circuit breaker, upstream auth
queue/ async index-regen queue; Mem (eval) + Postgres (HA) implementations
indexer/ npm packument regen worker (idempotent, queue-driven)
format/maven/ Maven 2: PUT/GET, checksum sidecars, maven-metadata.xml,
SNAPSHOT metadata, Gradle .module, parent-POM prefetch
format/npm/ npm registry: publish, packument, tarballs, dist-tags,
deprecate, unpublish, audit bridge, login, group fan-out
format/helm/ Helm repo: chart upload, index.yaml, chart API, OCI mode
format/cran/ CRAN: DESCRIPTION parse, PACKAGES + PACKAGES.gz + PACKAGES.rds
format/pypi/ PyPI: twine upload, PEP 503 simple index, pypi.org proxy, group
format/oci/ OCI Distribution Spec v1.0: blobs, manifests, tags, uploads
server/ HTTP router, auth middleware wiring, admin API, browse/search UI
obs/ Prometheus metrics, structured logging, audit log
conformance/ end-to-end conformance tests (real clients in Docker containers)
deploy/
helm/forge/ production Helm chart
helm/forge-stack/ all-in-one chart (forge + Postgres + SeaweedFS)
gitops/ ArgoCD Application + Flux HelmRelease examples
terraform/ AWS + GCP modules for cloud-managed Postgres/S3 (post-GA)
docs/
runbooks/ operations runbooks (backup, incident response, token mgmt, …)
security/ threat model, pen test scope
load/
smoke.js k6 load test: metadata GET p99 < 50ms, 50 concurrent publishes
soak.js 24h soak script (run manually pre-release)
Every push runs: lint (go vet, helm lint, terraform validate), unit tests
with -race, coverage gate (overall ≥75%, core packages ≥85%), integration tests
(Postgres + SeaweedFS S3 via testcontainers), conformance tests (all clients × formats),
SAST (gosec), dependency scan (govulncheck), and container scan (Trivy).
Nightly: full conformance matrix, DAST (ZAP baseline), k6 load test (SLO gate), kind cluster install + conformance smoke, timed quickstart gate (< 10 min).
forge scans stored and proxied npm and Maven artifacts against OSV.dev (built in, no setup) and surfaces findings on the Browse detail pane, the per-repo Security column, the dashboard tile, and the Security → Findings admin page. Hosted OCI/Docker images can also be scanned via Trivy — opt-in and off by default; it needs an operator-supplied trivy binary (see Setup → OCI image scanning). Other formats are labelled "not scanned" rather than shown a misleading green (CRAN has no credible advisory source; Helm is tracked separately).
A download policy can then warn on — or block — vulnerable downloads. Configure it under Security → Policies:
- A global default plus reusable named policies (e.g. "strict"), each with an enforcement mode (Off / Warn / Block), a severity threshold, a fail-open switch for never-scanned artifacts, and audited suppressions (silence a specific CVE/GHSA with a reason; who and when are recorded).
- Each repository's Security tab assigns a named policy or inherits the global default. Resolution is named policy → global default → Off. Policies are admin-set: enforcement is a security boundary, so consumers can't self-downgrade Block to Warn. Use Preview impact to see the blast radius (how many component@versions a policy would block) before enforcing.
Enforcement gates the artifact download itself (jar/tarball — never metadata or
checksums): Block returns 403 with an advisory link; Warn serves the
artifact but adds an X-Forge-Vulnerabilities response header. Both are audited
and emit a policy.violation webhook; blocks increment
forge_downloads_blocked_total.
Honest caveat — Warn is forge-side only. A warning shows in the UI, the
X-Forge-Vulnerabilitiesheader, the audit log, and metrics, but package managers don't render it —npm installand Maven won't surface it. Only Block is visible to the client, as a failed download. Treat Warn as a governance signal, not an install-time message.
forge can prove a repository's storage is internally consistent — the tool to
run after a migration, a restore from backup, or any time storage is suspect.
POST /api/v1/repos/{name}/verify (or the Verify now button on the repo's
Integrity tab / the Integrity admin rollup) enqueues a read-only
pass on the shared worker; findings are reported, never repaired automatically.
Four finding kinds, checked per format by the format plugin itself:
- missing — a metadata record (npm version, Helm chart, CRAN package, OCI manifest reference, Maven SNAPSHOT record) points at a blob that is gone. Clients see 404s.
- mismatch — stored bytes no longer match their recorded expectation:
Maven checksum sidecars, npm
dist.shasum/integrity, Helm chart digests, and OCI content-addresses are re-computed from the blob; CRAN (which stores no digest) is validated via the tarball's gzip CRC. - orphan — an object nothing owns: a blob absent from every index, a record whose content is gone, a stale OCI upload buffer.
- drift — a materialized index out of sync with its source records (npm's
packument).
POST /api/v1/repos/{name}/reindexrebuilds it.
Two modes: full (default) re-reads every blob that carries an expectation — IO-heavy but linear, and the only way to catch silent corruption; quick cross-references records and blobs without hashing. One report per repo is persisted (re-runs replace it); the UI shows verdict, per-kind counts, bytes read, and staleness. Repo-scoped admins can verify their own repos; the fleet rollup at Integrity is global-admin. Proxy-cache findings are hygiene only — caches re-fetch from upstream on demand. Group repos own no storage; verify their members.
A build resolving an internal package name through a group (or straight from a proxy) must never receive an attacker's same-named package from the public registry. forge enforces two ownership signals, on by default for every group and proxy repository:
- Auto-derived ownership — any name actually present in a hosted group member is pinned to hosted members in that group. The group's npm packument / Maven metadata / Helm index / CRAN PACKAGES carry only hosted versions of the name, and a version that exists only upstream is a 404, never an upstream fetch. Zero configuration; the classic Azure-Artifacts-style pinning.
- Explicit namespace claims — hosted repos declare what they own with
selector patterns (
@acme/**,com/acme/**,left-pad), closing the publish race: a claimed-but-not-yet-published name is refused with403 {"error":"blocked by dependency-confusion protection", "claim":…, "claimedBy":…}instead of falling through — on groups containing the claiming repo and on every same-format proxy repo, cached or not. Claims are set on the repo form / Settings tab or theclaimsfield of the repo API, validated at write time.
Blocked requests land in the audit log (dep-guard: blocked …), fire the
policy.violation webhook (kind=dependency-confusion), and count in
forge_depguard_blocked_total{repo}. Opt out per group/proxy repo with the
Dependency-confusion guard toggle (depConfusionGuard: false) to
deliberately merge internal and public versions of the same names. OCI has no
group support yet and is not guarded. scripts/depguard-validate.sh proves
the matrix live against registry.npmjs.org and Maven Central.
Every hosted repository can carry a storage quota (Storage quota (GB) on
the repo form, or quotaGB in the repo API). Once usage reaches the quota, the
next publish is refused in the request spine — before the format handler runs —
with 507 Insufficient Storage and a JSON body naming the used and quota bytes.
The refusal is recorded in the audit log (quota: blocked write …), fires the
policy.violation webhook (kind=quota), and counts in
forge_quota_blocked_total{repo}; forge_repo_quota_used_ratio{repo} gauges
current fill. Only hosted publishes count — proxy cache-fills are never
blocked (refusing to cache a transitive dependency would break a build; proxy
growth is bounded by cache eviction instead). Accounting is a documented soft
limit: usage comes from a periodic blob walk (re-run after every write) plus an
in-flight byte delta, so a burst of concurrent uploads can overrun the quota
slightly before the next walk reconciles it — exact within one walk cycle, with
no write-path ledger.
Deleting a version through the admin Delete button (or DELETE /api/v1/repos/{name}/component) is a soft delete: the artifact's blobs are
moved to a reserved _trash/ key space and the metadata a hard delete would
have removed is captured into a tombstone. Because trash lives outside every
repo's key space, deleting frees quota immediately and trashed bytes never
show up as integrity orphans; disk is reclaimed only on purge. The repo's
Content tab shows a Trash panel — Restore puts a version back exactly as
it was (npm packument entry included), Purge hard-deletes it. Trash is swept
automatically after -trash-retention (default 7 days; 0 keeps it until
manually purged). Format-native deletes and cleanup runs still hard-delete —
their job is to free space; trash is the human undo path. Prove the loop live
with scripts/quota-validate.sh (publish refused at 507, delete → trash →
restore → purge, cleanup still frees space).
Promote a tested artifact from one hosted repository to another — for example
staging → release — with POST /api/v1/repos/{target}/promote
({"sourceRepo","component","version"}), or the Promote… button on a
version in the source repo's Content tab. Promotion is a copy, not a
reference: the bytes are copied into the target through the target's own format
handler, so its indexes, checksums and packuments regenerate correctly and the
target stays completely self-contained — backup, cleanup, quotas and integrity
all keep working on it with no cross-repo coupling (a later cleanup of the
source can never pull bytes out from under a promoted release). All five formats
promote (maven asset trees, npm tarball + packument, helm charts, CRAN
tarballs, OCI images via a content-addressed manifest walk).
The copy records provenance — promoted from {sourceRepo}@sha256:{digest}
by {actor} at {time} — stored per target and surfaced on the component
detail pane. This is a historical fact: integrity verify deliberately does
not re-check a promoted copy against its source digest, because the copy is
independent by design. A promotion is a hosted write, so the target's storage
quota applies (a promote that won't fit is refused with 507); it is audited,
fires the artifact.promoted webhook, and counts in
forge_promotions_total{repo}. Admin on both source and target is required.
Mark a hosted repo Immutable (write-once) on the repo form (or
"immutable":true in the repo API) to make it the natural promotion target: an
existing component+version can never be overwritten — a re-publish or re-promote
returns 409, soft-delete is refused, and automated cleanup does not run (all of
which would let released bytes change). Publishing a new version is still
allowed. Prove the whole loop live with scripts/promote-validate.sh (24 checks:
byte-identical copy, provenance, immutable 409s, quota 507, npm packument regen,
clean integrity verify of the target).
forge imports repositories, content, and permissions from a live Nexus 3
server: POST /api/v1/migration/plan produces a persisted dry-run (every
source repo with a create/reuse/skip decision and reason, per-role grant
translation, and what will not migrate, stated plainly), apply runs the
import as a job on the shared worker, and the Migration admin page drives
the whole flow with live per-repo progress. All five formats transfer through
forge's own format handlers (maven asset paths + checksum sidecars, npm
per-version publishes rebuilt from the source packument, helm charts, CRAN
tarballs, docker via a registry-protocol copy that preserves digests); proxy
repos migrate configuration only, and Nexus roles/privileges/content
selectors become grant-carrying forge roles. After each repo the importer
records source-vs-target counts and enqueues a full integrity verify —
migration is done when counts match and verify says intact. Re-applying
resumes idempotently. Details: docs/runbooks/nexus-migration.md.
- OIDC SSO — shipped: login against Keycloak/Entra/Okta/ADFS with group→role mapping (see Single sign-on). Direct LDAP/AD bind (no IdP broker) and SAML remain post-GA; the role-mapping layer is factored so an LDAP frontend slots in without rework.
- CRAN binary trees (
/bin/) — per-OS pre-compiled packages; source packages work for current use cases. - Distributed tracing — OpenTelemetry integration; Prometheus metrics + structured logs cover current operational needs.
- Chaos suite — automated pod-kill and S3/PG-blip recovery tests.
- Cloud Terraform modules — AWS + GCP modules exist; Azure and nightly apply/destroy validation are post-GA.