- npm rebuilds a packument concurrently (#956 follow-up) —
regenerate_packumentwalkedversions/anddist-tags/one key at a time. On an object store each key is a round-trip, and #956 put that walk on a read path, so the first request after a packument went missing paid one round-trip per version: 505 reads and 0.77 s for a 500-version package against a loopback S3 store, linear from there. It now reads them through the same buffered fan-out the RPM and Deb index rebuilds use, keeping the lenient behaviour — an unreadable or unparsable version is skipped rather than failing the rebuild. - Every storage round-trip is counted (#969 follow-up) —
stat,pin,listandlist_with_metareached the backend without touchingnora_storage_operations_total, which is why a handler issuing onestat()per file (tens of thousands of HEAD requests on a single PyPI index response) moved no metric and could only be found by reading code. All four now increment it; onstat/pinan absent object or an unpinned one isstatus="miss", so ordinary misses do not inflate error-rate alerting. Counters only — no behavioural change. - Changelog entries live in
changelog.d/(#971 follow-up) —## [Unreleased]inCHANGELOG.mdis no longer edited by hand. Each change addschangelog.d/<number>.<category>.md, andscripts/changelog-fragments.shassembles them (--check,--render,--apply,--release <version>). Three pull requests merged into 1.3.1 each added a bullet to the same few lines and each conflicted with the previous one; separate files cannot. A.gitattributesunion merge was the lighter alternative and was rejected: it duplicates the### Fixedheading when two branches both introduce it, and it is unclear whether the merge performed on the forge honours the driver at all, which is where the conflicts actually surfaced.
- PyPI simple JSON no longer stats every file (#969) — the PEP 700 fields added in 1.3.0 made
GET /simple/{name}/with a JSONAcceptheader issue one storagestat()per file in the index, serially. On an object store that is a HEAD round-trip each, so the response time scaled with the number of files the index lists and uv — which always negotiates the JSON index — hit its timeout; the HTML index pip uses was never on that path. Each file’ssizenow comes from the listing the handler already performs, and the cacheddates.jsonis read once per response instead of twice. Measured on S3 against a 40 000-file proxied index:uv lockwent from 129.8 s and 59 764 storage HEAD requests, failing after three retries, to 0.5 s and 2. A second regression on the same path is fixed with it: an upstream that answers the PEP 691 JSON withoutupload-timeleft nothing cached, so every request re-downloaded the whole upstream index; it is now recorded once, while a failed fetch still caches nothing so a transient outage stays retryable. lock-auditno longer depends on gawk (#971 follow-up) — Checks 1 and 3 used gawk's three-argumentmatch(), which mawk rejects. mawk is the defaultawkon Debian, Ubuntu and the CI images, and an aborted awk prints nothing — so on any machine without gawk the audit reported no findings and looked clean. The awk programs are now POSIX, andscripts/test-lock-audit.shruns the audit under every implementation it finds, asserting they agree and that each run still produced the known finding, so "both silent because both aborted" cannot pass.lock-auditno longer reports a guard that cannot drop early (#971) — Check 2 scanned forward from the end of a guarded block to the next column-0}and flagged the first storage write it met, with no notion of branch exclusivity. Apublish_locktaken underif !is_tarballwas therefore reported against a write underif is_tarball, which no request can reach on the same path. The scan now skips a write whose enclosingifchain contains the textual negation of a condition enclosing the guard. Where the reasoning is beyond textual analysis, a// LOCK-SAFE: <reason>comment inside the block of the write exempts it, with the reason recorded next to the code; a bare marker with no reason silences nothing, and the function-level markers the repo already carries are deliberately not honoured, because a function-wide exemption would hide a genuine finding elsewhere in the same handler.scripts/test-lock-audit.shpins all four directions.- npm no longer caches a proxied packument over a locally owned key (#975) —
npm/{name}/metadata.jsonis the assembled form of keys this registry owns, but the proxy branch cached the upstream document there unconditionally. On the metadata path that branch is reached with local versions present only by falling through the rebuild — its write failed, or the read-back missed — and the cached upstream document then dropped every locally published version from the packument. It did not self-heal, because the read-path rebuild triggers on the key being absent and it now existed with the wrong content, sonpm install pkg@<locally-published-version>failed until the next publish. The write also landed unserialized against thepublish_lockreleased a few lines above, so a concurrent publish could be overwritten. The upstream bytes are still served for that request; only the write is skipped, and the next request retries the rebuild.
- PyPI Simple JSON carries PEP 700 fields (#896) — the PEP 691 JSON response (
application/vnd.pypi.simple.v1+json) now also emitsmeta.api-version: "1.1", a project-levelversions[]list, and per-fileupload-time(RFC 3339) andsize(bytes), so tools like Renovate can compute a minimum release age without fetching every file.sizeis emitted for locally stored artifacts andupload-timefrom the cached upstream dates on the proxy path — each is included where known. - Hash pins on S3/GCS via object metadata — the SHA-256 integrity pin is no longer a local-filesystem-only feature. On object-store backends it is written as the user-defined
sha256object metadata, atomically with the object, and read back on GET/HEAD, so buffered reads verify at rest and raw files getETag,If-None-Match(304) andIf-Matchconditional overwrite on every backend. Pins are now a backend concern: the local backend keeps its NDJSON sidecar (same path and format, no migration), the object-store backend keeps object metadata, and the storage wrapper only validates keys and runs the fail-closed verify gate. Objects written before the upgrade carry no metadata and stay open-world until they are rewritten;nora re-pinrewrites the object on an object store, since object metadata cannot be changed in place. - Raw upload integrity via
Repr-Digest(RFC 9530) — a rawPUTmay declareRepr-Digest: sha-256=:BASE64:; NORA verifies the received body against it before committing, so a corrupted or truncated upload is rejected with400instead of being pinned. The pin itself is always the server-computed hash; the header only gates the commit. ARepr-Digestwithout a sha-256 entry is rejected rather than silently skipped. - npm serves the abbreviated packument to installers —
npm installasks forapplication/vnd.npm.install-v1+json, and NORA ignored it and returned the full document to every client. The packument path now projects to npm's abbreviated shape, keeping the per-version fields an installer actually resolves on (dependencies,os,cpu,engines,peerDependenciesMeta,dist,deprecated) and dropping readme, maintainers, repository, per-versiondescription,scriptsandgitHead. Measured against a live upstream with every version preserved: lodash 247 652 → 71 989 B (−70.9%), express 804 975 → 344 703 B (−57.2%). The short form is derived locally rather than requested upstream, so exactly one canonical object stays cached per package and a short document can never displace the full one;Vary: Acceptgoes with it, because metadata isCache-Control: publicand the body now varies by a request header. An unparsable body is served unchanged rather than turned into an error (#957).
- Compile-time integrity witness on the streaming serve path (#849) — the streaming artifact serve now routes through a sealed sole-sink whose only constructor takes an EOF-verifying stream, so handing a raw reader to the response body on an integrity path is a compile error — the type-level match of the buffered
verified_bodysink. A blob tampered on disk aborts the body mid-stream (the client gets a broken transfer, never the tampered bytes under a clean200) instead of streaming out unverified; explicit partial-content range serves take a separate open-world sink. - Registry dispatch is keyed on the
RegistryTypeenum (#369) — dispatch across config, retention, metrics and the UI is now an exhaustivematch RegistryTypegenerated from one list, instead of scattered string comparisons. Adding a format is a single line and can no longer silently miss a call site (it becomes a compile error). No behavioral change. - Blocklist rules with a literal name are looked up by name (#953) —
BlocklistFilter::evaluateran three glob matches per rule on every artifact download, which is free for a hand-written file of a few dozen rules and not free once the file is generated. A rule whosenameis a literal now sits in a hash map and a rule whose name is a pattern stays in a scan; the winner is still the first matching rule in file order, so thereasona client reads in the 403 does not change.
- The repository signing key is never enumerated by storage
list()(#891) — the OpenPGP signing key at<storage.path>/.signing/nora.key(persisted owner-only,0600) was swept into every enumeration-based operation becauselist()/list_with_meta()excluded only the pin sidecar:backupwrote it into the tar at0644,migrate --to s3copied it into the bucket as a plaintext object, and GC/retention and the browse UI treated it as an artifact. Both backends now exclude the.signing/prefix from enumeration, so the key can neither be exfiltrated (tar / object) nor deleted; it is loaded via direct filesystem I/O and never throughlist(), so there is no runtime impact. Provision the key out-of-band.
- npm rebuilds a missing packument instead of answering 404 — a hosted package whose derived
metadata.jsonwas absent returned404while every published version was still sitting in storage. The reassembly already existed (regenerate_packument, which listsversions/,dist-tags/andpkg.json) but only the publish path reached it, so a read fell through to the upstream proxy and 404'd for a package that exists only in this registry. The read path now rebuilds whenversions/is non-empty, serves the result and re-materializes the packument so the cost is paid once — under the samepublish_lockas publish, so a fleet stampeding one package rebuilds it once rather than once per request, and before the namespace guard, because serving locally-owned bytes is always allowed while that guard exists to stop the upstream fetch. A name with nothing behind it still returns 404. Newnora_packument_rebuilt_total{registry}: a non-zero rate means storage was written or restored outside NORA (#956). - Docker GC keeps the digest alias of a tagged manifest (#949) — since tag-rooted GC shipped in 1.2.2, a GC run deleted
manifests/sha256:<digest>.jsonwhilemanifests/<tag>.jsonwith the same bytes survived, so pulling a tagged image by digest returned 404 afterwards. The mark phase now roots each tag manifest's digest alias, and an orphaned digest manifest takes its.meta.jsonsidecar with it instead of leaking it forever. - Retention rebuilds the npm packument after deleting versions (#961) — npm retention deleted a version's tarball and
.sha256sidecar and nothing else, so the per-version document and the packument entry survived and the registry kept advertising versions it could no longer deliver. The version's key set now includesnpm/<pkg>/versions/<v>.json, and the packument is regenerated afterwards under the same publish lock, the way retention already rebuilds RPM and Debian indexes.
- Size-based eviction for proxy-cached RPM and Debian files (#866) —
[gc] proxy_cache_max_bytes(NORA_GC_PROXY_CACHE_MAX_BYTES, default0= off) makes GC evict the oldest proxy-cached files by mtime until the cache is within the budget. Hosted packages and repository index files (repodata/,Packages,Release) are never evicted. New metricsnora_gc_proxy_cache_evicted_totalandnora_gc_proxy_cache_bytes_freed_total;nora gcprints an eviction summary.
- Docker GC is rooted at tags (#655) — GC built the referenced-blob set from every stored manifest, so a re-pushed tag left its previous digest manifest behind, and that manifest kept its layers referenced forever. The set is now built from tag manifests only, so digest manifests that no tag reaches, and the layers only they hold, are reclaimed.
- Maven proxy metadata is not rewritten when nothing changed (#888) — under a low or zero
metadata_ttl, every metadata request wrote the mergedmaven-metadata.xmland its four checksums back to storage even when the merged document was byte-identical to the cached one. Those five writes are now skipped when nothing changed.
- Anonymous dashboard callers no longer see proxy upstreams (#934) — with
anonymous_read = truethe dashboard API listed every mount point's upstream registries to unauthenticated callers, disclosing the proxy topology. Unauthenticated responses now carry an emptyproxy_upstreams, and credentials sent to a publicly browsable page are validated so authenticated users still see them.
- Docker retention counts only tags toward
keep_last(#932) — every push stores a manifest under both its tag and its digest, and retention counted both, sokeep_last = 3on four tags kept two. Digest references are now skipped by retention and filtered out of the tag list API. - PyPI follows relative links in upstream mirror indexes (#877) — mirrors that return relative
hrefs in the simple index (for example../../packages/torch-2.4.0.whl) failed because the raw relative path was rejected as an invalid URL. Links are now resolved against the index page URL.
- Retention
exclude_tagsno longer uses up thekeep_lastbudget (#926) — excluded versions took positions in the sorted version list without being counted as kept, so when they sat at the top, versions below them were pushed past thekeep_lastthreshold and deleted. Only non-excluded versions now count towardkeep_last. - GC no longer prunes npm metadata in proxy mode (#925) — tarballs of a proxied package are cached on demand, so a missing local tarball is expected, but GC treated it as an orphan and removed the version from the cached metadata. The npm phantom cleanup is skipped when npm runs as a proxy.
- npm revalidation no longer loops on a missing cached body (#867) — a
304 Not Modifiedwhose cached body was gone returned nothing but left the.metavalidators in place, so every TTL cycle sent the same validators, got another 304 and never recovered. The stale validators are now deleted so the next request fetches the document again.
- Docker cross-repo blob mount (
?mount=&from=) —POST /v2/{name}/blobs/uploads/?mount={digest}&from={repo}copies a blob from another repository on the same registry instead of re-uploading it. Returns201 CreatedwithLocationon success, falls back to a regular upload session if the source blob is missing (#917). - Resumable downloads (
Range/206 Partial Content) for every format — what Docker blob GET gained in #657 now covers all artifact payloads: maven release artifacts, npm tarballs, pypi files, gems, cargo.crates, go module zips, nuget.nupkgs, terraform provider/module archives, conan blobs, deb/rpm packages, ansible collections, pub archives, and raw files. A single-range request is served straight from the storage backend's native ranged read (local file seek, S3/GCS ranged GET) via a shared helper, so an interruptedcurl -C -/pip/apt download resumes instead of restarting; a resume at end-of-file gets the RFC 9110416+Content-Range: bytes */{size}that tells the client it already has everything (previously Docker re-served the full blob), and a failed ranged read falls back to the full 200 instead of a 500. Full-200 artifact responses advertiseAccept-Ranges: bytes. Mutable content (maven-metadata.xml, packuments, indexes,dists/,repodata/) neither advertises nor honors ranges — a resumed range across a rewrite would splice two generations. For the same reasonraw, the one overwritable format, honorsIf-Rangeagainst its pin ETag. A partial body cannot be re-hashed, so a ranged serve carries no server-side integrity check (the #657 precedent — the client's own lockfile/checksum covers it), and on formats where the digest-quarantine gate needs the whole object a range request under an active quarantine policy falls back to the gated full response rather than bypassing it (#893). - CI builds a per-PR test image — every non-fork PR now pushes
ghcr.io/getnora-io/nora:pr-<number>(alpine, amd64) after thetestjob passes, and auto-comments thedocker pull/docker runcommand on the PR so reviewers can test without building locally. Fork PRs are skipped (nopackages:writetoken) (#908). - Per-PR test images are now garbage-collected — a
pr-image-cleanupworkflow deletespr-<number>from GHCR when its PR closes, plus a daily sweep removes anypr-*orphan older than 7 days (GHCR has no native tag TTL) (#910).
- Maven keeps server-generated artifact metadata authoritative — a Maven client that re-uploads a stale artifact-level
maven-metadata.xmlafter a concurrent deploy no longer overwrites the version list NORA generates: an uploaded artifact-level metadata document (and its checksums) is recognized by its shape and dropped, while version-level (SNAPSHOT) and group-level (plugin) metadata are still stored verbatim. On a proxy refresh, locally hosted versions are merged into the refreshed upstream document instead of being replaced by it, and the.md5/.sha1/.sha256/.sha512sidecars are recomputed from the merged document. The proxy-side merge runs under the samepublish_lockas the upload-side regeneration, so the document and its checksums are written as one critical section and stay mutually consistent under concurrent fetch and publish (#887). - Docker returns OCI-conformant 429 for concurrent-upload limit —
POST /v2/{name}/blobs/uploads/now returns an OCITOOMANYREQUESTSerror body withRetry-Afterheader when the concurrent-upload ceiling is reached, instead of a plain status code that some clients could not parse (#895). - Object-store timeouts sized for client-paced streaming —
putandgetoperations on the object-store backend now use timeouts proportional to the expected transfer size, so large blob uploads on slow links no longer time out spuriously (#894). - Cancelling a blob upload frees the session instead of leaking it —
DELETE /v2/{name}/blobs/uploads/{uuid}, the OCI cancel verb, was never routed: the upload dispatcher matched onlyPATCHandPUT, so a client that correctly cancelled got405 Method Not Allowedand its session stayed in the map until the 30-minute TTL, still holding one ofmax_upload_sessions.DELETEnow removes the session and its temp file and answers204 No Content. Two supporting fixes: a rejectedPOSTno longer leaves behind the zero-byte temp file, and the429'sRetry-Afteris jittered over 3–10 s (#897). - npm self-prime metadata on tarball download —
ensure_npm_metadata_cachedfetches and caches the packument on a tarball cache-miss sotrust_upstream_datesmatures old npm artifacts correctly; includesis_internal_namespaceguard (#68) (#903). - PyPI internal bookkeeping files excluded from simple index and downloads —
dates.json(used bytrust_upstream_datesquarantine) was leaking into PEP 503/691 package listings and was directly downloadable; strict clients likeuvrequire hashes on every file entry, causing parse failures. Both listing and download paths now filter viais_valid_pypi_filename()(#891). - PyPI
ensure_pypi_dates_cachedno longer leaks internal package names upstream — the function was missing theis_internal_namespaceguard that Cargo and npm already had, causing internal-namespace package names to be sent to external upstream registries (#68 dependency confusion surface) (#905). - Conan v1/ping route added — Conan 2.x client hard-codes
GET /v1/pinginClientV2Router.ping()before any v2 API calls; the missing route returned 404, causing the client to refuse further interaction (#901). - Docker fails closed on transient storage errors in manifest reads — a transient storage error during manifest GET now returns 500 instead of 404, preventing clients from interpreting a storage hiccup as a missing manifest (#911).
- Atomic token-file writes; store errors answer 503, not 401 — token CRUD operations use atomic file writes (write-to-temp + rename), and a storage I/O error during token validation now returns
503 Service Unavailableinstead of401 Unauthorizedwhich would cause clients to discard valid credentials (#912). - Retention and GC unified into one scheduler — retention and GC no longer race for the cleanup lock; a single scheduler runs retention first, then GC, so retention actually executes instead of being starved by GC (#914).
- rpm/deb index-rebuild sidecars read concurrently — sidecar files are read in parallel during index regeneration, reducing rebuild time on repositories with many packages (#913).
- h2 updated 0.4.13 → 0.4.17 — addresses RUSTSEC-2026-0258 (#916).
- README: expanded supported registries table and fixed legend link (#900).
- Documented
docker_anon_pullin env example and llms.txt (#899).
- Ansible collections with more than 100 versions now install through the proxy —
ansible-galaxypages a collection's version list atlimit=100, and galaxy_ng returns thelinks.next/first/lastpagination pointers as host-relative paths (/api/v3/plugin/…/versions/?offset=100, no scheme or host). The metadata URL rewriter only rewrote absolute upstream URLs, so those relative links passed through untouched;ansible-galaxythen resolved them against NORA's host root, dropping the/ansiblemount prefix, and every request for page 2+ 404'd — surfacing asError when getting available collection versions for <ns>.<name> … (HTTP Code: 404)(e.g.community.docker, 148 versions). The rewriter now also maps the root-relative pulp paths to root-relative NORA paths (/api/v3/plugin/…/index/→/ansible/v3/collections/,…/artifacts/→/ansible/download/), kept path-only so the client's relative-link resolution keeps the mount. Collections with ≤100 versions never paginated and were unaffected. - Retention and GC schedulers run once at boot — both schedulers used to wait a full interval before their first pass, so a process restarting more often than the interval (rolling deployments, crash loops) never ran either of them, accumulating unbounded garbage while the schedule looked configured. The interval's first tick now fires immediately, and the boot pass waits on the shared cleanup lock (instead of skip-if-held) so retention and GC don't race each other out of their first run.
- Cargo sparse index now advertises
auth-requiredon private deployments — with auth enabled andanonymous_readoff,/cargo/index/config.jsonsets"auth-required": true(RFC 3139) so cargo sends credentials on index and download requests. Previously cargo only authenticated the publish API, and every sparse-index fetch against a private instance failed with 401 before publish even started. - Scoped npm packages round-trip on S3 / object_store backends —
encode_object_keypercent-encodes@→%40for the storage key, butobject_store::path::Path::fromthen treats that key as a to-be-encoded path and percent-encodes the%again to%2540(itsINVALIDset includes%, not@). On read-back the key never decoded, so a scoped package'slist+getfound nothing andregenerate_packumentwrote an emptyversionsmap —npm install @scope/pkgfailed withNo matching version found/ENOVERSIONSeven though every version tarball was stored intact.decode_object_keynow reverses the double-encoding (%2540→@before%40→@), and publish fails closed (500, nothing committed) if the just-published version isn't visible in the packument it regenerated. The local-filesystem backend never double-encoded and was unaffected; reproduced on MinIO, SeaweedFS, RustFS and Garage. - Docker serves locally-pushed manifests without upstream revalidation — a manifest pushed to a proxy-enabled repository is now served from local storage directly, instead of being revalidated against (and shadowed by a 404 from) the upstream.
registries.enableback-propagates to the per-registry flags — setting the consolidatedregistries.enablelist now flips each named format's ownenabledflag, so enabling rpm/deb through it no longer leaves those routes returning 404.- Object-store reachability cache self-expires on a stalled refresh — the cached reachability verdict is now treated as stale past a max age even if the background refresh loop stops advancing it, so a wedged refresher can't pin health to a permanently-stale answer.
/healthand/readyno longer probe object storage — the liveness/readiness endpoints answer from process state instead of issuing an object-store round-trip per probe, so a slow or throttled backend can't turn a health check into a failed probe.
- Pull-through proxy and offline mirror for rpm and deb — the last two hosted-only formats gain the proxy mode every other format has. A repository name maps to a single upstream repo (
[rpm.proxies] fedora = "https://dl.fedoraproject.org/…/os",[deb.proxies] debian = "https://deb.debian.org/debian", string or{ url, auth };NORA_{RPM,DEB}_PROXIES="repo=url|auth,…") — per-repo rather than a flat upstream list because rpm/deb have no global coordinate space, and exactly one upstream per repo because mirrors of the same distro repo lag each other and mixing them within a TTL window can serve a repomd.xml whose referenced blobs belong to a different sync generation. A proxied repo is read-only (publish/delete/reindex → 409); upstream metadata —repodata/,dists/indexes, and the upstream's own signatures and keys — is served verbatim withinmetadata_ttlseconds (default 300, non-positive revalidates every pull,Cache-Control: no-cacheto clients), packages (.rpm/.drpm,.deb/.udeb) are cached immutably, and when the upstream is down the stale cache is served withx-nora-stale: true. The proxy path reuses the shared machinery: circuit breaker, upstream-hostname leak detection, cache-hit/miss metrics, and the digest quarantine ([curation.rpm]/[curation.deb]overriding the globalcuration.quarantine) gating packages — never the mutable indexes. For air-gapped clients,nora mirror rpm --repo <name> [--arch x86_64,noarch]andnora mirror deb --repo <name> [--dist bookworm] [--component main] [--arch amd64]enumerate the upstream package list through the proxy (repomd.xml → primary.xml; Release → Packages,.gz/.xz/plain) and pre-fetch every package, skipping what the cache already holds — after a run, dnf/apt clients work with the upstream unreachable. - Retention over rpm, deb, and raw — the three formats retention previously skipped silently. rpm/deb versions are collected from the metadata sidecars (payloads never read) and grouped per
{repo}/{arch}/{package}— structured-layout deb packages per{repo}/{distribution}/{component}/{arch}/{package}, sokeep_lastcounts within each distribution × architecture's independent index (all/noarchpackages form their own group); deleting a version removes the package and its sidecar, and every touched repo's indexes are rebuilt and re-signed under the publish lock afterwards — retention can no longer leave a signed index advertising deleted packages. Raw groups depth-2 path prefixes (raw/{name}/{version}/…) as the aging unit, so a directory of related files ages out together; root-level files are never collected. New optionalname_globon retention rules targets groups within a registry (e.g.*-dev-*/*for an age-only policy on dev repositories,*-stream-*/*for a keep-last window) — first matching rule wins, and no matching rule still means keep forever.nora retention-apply --yessigns regenerated indexes with the same key as the server. - Per-rule namespace scope for OIDC role rules — an
[[auth.oidc.providers.role_rules]]entry may setnamespace_scope = ["ci-transport/**"]to narrow the provider's scope for identities matched by that rule. A write must satisfy both the provider scope and the rule scope — the provider scope stays a hard ceiling, and a rule cannot widen past it. Lets one issuer grant, e.g., pull-request CI builds write access confined to a transport prefix while main/tag builds keep the provider-wide scope. Absent = inherit the provider'snamespace_scope; enforcement mode stays provider-level. Also corrects the config doc example forrole_rules, which showed a map form that fails to parse (the real shape is an array of tables withpattern/role). - Intra-segment
*wildcards innamespace_scope— scope patterns now match a*within a single path segment (e.g.team-*/ci), not only the**cross-segment form, so a scope can target a naming convention inside one level without opening the whole subtree. - A geo/policy-blocked upstream is now observable — when a proxied upstream answers a fetch with a 4xx carrying a policy/geo block signature (
x-amzn-waf-reason, as an AWS CloudFront + WAF geo-block does), NORA emits awarnlog naming the registry and reason and incrementsnora_upstream_policy_blocked_total{registry,reason}, instead of relaying an anonymous 404 indistinguishable from a genuinely absent artifact. The relayed 404 status and the circuit breaker are deliberately unchanged — a policy block is not an availability failure, so it must not trip the breaker — but it is no longer silent.
- The browse web UI is now gated on a private deployment. With auth enabled and
anonymous_readoff, the UI, its JSON API (/ui,/api/ui), and the API docs (/api-docs) were served without authentication — enumerating every repository and package a private registry exists to hide. They now require credentials unlessanonymous_read(which already exposes the same names through the registry read APIs) or the newauth.public_web_ui(NORA_AUTH_PUBLIC_WEB_UI, default false) opens them; an unauthenticated request gets a Basic challenge so browsers prompt. Health/readiness probes stay unconditionally public./metricsgets its ownauth.public_metrics(NORA_AUTH_PUBLIC_METRICS, default true — scrapers rarely carry credentials and labels name registry formats, not repositories); set it false to gate metrics too. Behavior change: operators who relied on an anonymous web UI while keeping the registry APIs authenticated must setpublic_web_ui = true(or enableanonymous_read).
- Structured apt repository layout (
dists/{distribution}/{component}) — deb repositories can now publish the canonical suite layout in addition to (or instead of) the flat one, chosen per package at upload:PUT /deb/{repo}/{path}?distribution=jammy&component=main(component defaults tomain) places the package underdists/jammy/, with per-distributionRelease/InRelease/Release.gpgand{component}/binary-{arch}/Packages{,.gz}generated per component×architecture (empty combinations included, so a Release never references a missing index; arch-allpackages fold into every concrete architecture). The upload path stays free-form —Filename:entries are repo-root-relative, so apool/tree is conventional, not required. Uploads withoutdistributionkeep the existing flat behavior, both layouts can coexist in one repository, and deleting the last package of a distribution removes its wholedists/tree (stale signed indexes would otherwise keep advertising it). Sources line:deb [signed-by=…] {url}/deb/{repo} jammy main. Verified end-to-end against apt on Debian: update, install from two components, and hard rejection of an unverifiable InRelease. - rpm/deb repository reconcile (
POST /{rpm,deb}/{repo}/-/reindex) — heals a repository whose storage changed behind the API's back (filesystem-is-the-database, ADR-2): sidecars whose package is gone are dropped, packages with no sidecar are parsed and adopted (same validation as the upload path — an invalid file fails the reconcile with 422 rather than being silently skipped), and the repo's indexes are rebuilt and re-signed under the publish lock. Returns JSON counts. Also serves as the re-sign hook after a signing-key change: reindex every repo and clients verify against the new key. The index regeneration internals are now callable without request state, groundwork for retention over rpm/deb. - Native Google Cloud Storage backend (
storage.mode = "gcs") — previously GCS was reachable only through its S3-interoperability layer with static HMAC keys. The S3 backend is generalized into anObjectStorageover theobject_storetrait (the S3 path is unchanged) plus a GCS constructor: bucket fromstorage.bucket, credentials resolved as explicit service-account JSON (storage.gcs_service_account_path/NORA_STORAGE_GCS_SERVICE_ACCOUNT_PATH), then ambientGOOGLE_*env, then the instance metadata server — so GKE Workload Identity and GCE service accounts work with no key material.storage.gcs_base_urloverrides the endpoint for emulators or Private Google Access (anhttp://override also skips request signing).NORA_STORAGE_MODE=gcs;nora migrateacceptsgcsas source/destination. Same single-writer caveat as S3 for rpm/deb publishing. Verified end-to-end against real GCS: dnf and apt both install from a GCS-backed instance (#128 follow-on). - Streaming raw uploads —
PUT /raw/{path}no longer buffers the request body in memory: the body streams frame-by-frame to a temp file on the storage filesystem (O(frame) peak RAM), is hashed for the integrity pin, and commits viaput_from_path— a same-filesystem rename on the local backend, a streaming multipart write on object stores.raw.max_file_sizeis enforced incrementally as frames arrive (plus a fast reject on an oversized declaredContent-Length), so it now governs uploads of any size independent ofserver.body_limit_mb, which no longer applies to raw uploads. Conditional PUT semantics (If-Match/If-None-Match, ETag, immutability) are unchanged; a crashed or aborted upload leaves no temp file (RAII cleanup, verified in tests). The Docker blob-upload streaming helpers moved to shared registry code and Docker's digest verification reuses the shared file-hash path.
nora import— one-shot migration from Artifactory & Nexus — a stateless CLI (nora import assess/nora import run) that pulls repos + artifacts straight into NORA storage over HTTP, filesystem-resumable, as a single static binary with no mandatory runtime dependency. Forward-only source adapters (Artifactory AQL, Nexus continuation-token); each artifact streams through the proxy pipeline (peak RAM O(chunk)) with verify-before-commit fail-closed on checksum mismatch, full curation applied, and atomic commit — imported keys reuse the handlers' own key format so GC/retention/UI see them. Resume is an on-disk.donemarker + NDJSON journal (no DB, ADR-2); reruns are idempotent. SSRF-guarded client (DNS-pinning, per-redirect-hop IP deny-check; blocks loopback/private/link-local/CGNAT/metadata incl. v4-mapped and NAT64). Optional--with-permissionsemits an inert permission-proposal report (never live credentials, never above Read without--grant-write). On an S3 target the sha256 pin is not recorded — a loud WARN flags transfer-integrity-only, not at-rest. Full phased batch migration (#172) tracked post-1.0. (#599)- GPG-signed rpm/deb repository indexes — closes the
[trusted=yes]/gpgcheck=0gap (#128). A per-instance OpenPGP key (v4 Ed25519, the variant every deployed gpgv/gnupg verifier understands) is generated at first boot and persisted under<storage.path>/.signing/nora.key(owner-only, atomic write;signing.key_path/NORA_SIGNING_KEY_PATHoverrides — required for S3 storage, where signing is otherwise disabled with a warning). Every repodata/index regeneration also writesrepodata/repomd.xml.asc(rpm) andInRelease+Release.gpg(deb), fail-closed and ordered after the files they sign; public keys are served atrepodata/repomd.xml.keyandpubkey.gpg. Turning signing off removes stale signatures on the next regeneration (a mismatched leftover would hard-fail clients), and a present-but-corrupt key is a fatal startup error — never silently rotated. Verified end-to-end with verification enforced: dnfrepo_gpgcheck=1(Fedora 41) and aptsigned-by(Debian 12) both install and hard-reject a wrong key.signing.enabled = falserestores the previous unsigned behavior. - RPM registry (yum/dnf, hosted) — 14th format at
/rpm/. Each/rpm/{repo}/is an independent hosted repository:PUT {repo}/{name}.rpmparses the package header server-side (pure-Rustrpmcrate) and regeneratesrepodata/(repomd.xml + sha256-named primary/filelists/other.xml.gz);DELETEregenerates. Rebuilds run under the per-repo publish lock, fail closed, and prune unreferenced repodata generations. Repodata is unsigned — clients setgpgcheck=0 repo_gpgcheck=0; GPG signing is tracked in #128. Default-disabled (NORA_RPM_ENABLED=true), hosted-only. Verified end-to-end against dnf on Fedora 41 (#128). - Debian/APT registry (hosted flat repos) — 15th format at
/deb/. Each/deb/{repo}/is an independent flat repository (deb [trusted=yes] {url}/deb/{repo} ./):PUT {repo}/{name}.debparses the control paragraph server-side (ar → control.tar.{,gz,xz,zst}; pure-Rustar/lzma-rs/ruzstd, decompression bounded) and regeneratesPackages,Packages.gz, andRelease;DELETEregenerates. Rebuilds read per-package control sidecars under the per-repo publish lock and fail closed. Indexes are unsigned — clients use[trusted=yes]; GPG signing is tracked in #128. Default-disabled (NORA_DEB_ENABLED=true), hosted-only. Verified end-to-end against apt on Debian (#128). - Namespace-isolation refusals observable in Prometheus — cross-namespace internal-artifact refusals are counted as a labeled metric for alerting (#821 follow-up, #823).
- Token storage under systemd — the relative default token-store path escaped the systemd sandbox and failed to persist; it now resolves correctly under a hardened unit (#818).
- Bounded memory on large blob uploads — Docker blob uploads stream to disk instead of buffering the whole layer, bounding peak RAM on large images (#819).
- Isolated-namespace Docker miss returns 404, not 403 — a manifest/blob miss in an internal namespace no longer leaks existence via a 403 (#821, #822).
- Upstream URL scrubbed from ansible/nuget rewrites — slash-escaped upstream URLs are stripped so proxied Ansible/NuGet responses don't leak the origin (air-gap hygiene) (#385, #824).
- UI — corrected Russian labels and a zero-dependencies footer note (#820).
- Admin-gated admin-token minting (
POST /api/v1/admin/tokens) — a dedicated route, reachable only behind the/api/v1/admin/gate (auth::is_admin_path), that mints an API token of any role includingadminfor a given subject without theauth.admin_usersself-service check. Anonymous,anonymous_read, Basic-auth (no role) and Read/Write callers are denied fail-closed before the handler;ttl_days = 0is rejected and every mint is audit-logged (actor, target, role, ttl — never the token).auth.admin_users(NORA_AUTH_ADMIN_USERS) thus becomes a bootstrap-only fallback on the unchanged publicPOST /api/tokensroute, so GHSA-78cx-cfhm-rgmx stays closed; with auth disabled the route returns503(#746, #808). npm auditproxied to upstream for remote repos —npm auditPOSTs to/-/npm/v1/security/advisories/bulk(npm7) or/-/npm/v1/security/audits/quick(npm6), which previously hit the405fallback and failed. NORA keeps no advisory database, so for a proxy repo it now forwards the request to the configured upstream and returns the response verbatim. Both audit POSTs are read-eligible underauth.anonymous_read, so anonymousnpm auditworks wherever anonymous install does; non-audit npm POSTs stay gated. Under an activeinternal_namespacesfilter thebulkrequest strips internal-package keys before forwarding and fail-closes (200 {}) on any body it cannot verify, the gzippedquicklockfile is refused wholesale, the clientAuthorizationis never forwarded, and the body is bounded at 8 MB; upstream 5xx/network →502, circuit-open →503, no proxy configured →200 {}(#597, #805).- S3 virtual-hosted-style addressing (
storage.s3_virtual_hosted,NORA_STORAGE_S3_VIRTUAL_HOSTED) — some S3-compatible providers reject signed path-style requests (Alibaba Cloud OSS answers403 SecondLevelDomainForbidden), which made the S3 backend unusable there because the addressing style was hardcoded to path-style. A new default-off toggle threads throughStorage::new_s3/S3Storage::newintoAmazonS3Builder::with_virtual_hosted_style_request; when enabled,object_storeuses the configured endpoint verbatim, so it must include the bucket host. Default (false) preserves current path-style behavior (#795, #798). - Chinese (Simplified) UI translation — a
中文entry in the language switcher backed by a fullzhtranslation table; language detection now normalizes BCP-47 / POSIX tags to their primary subtag, sozh-CN,zh-Hansandru_RU.UTF-8resolve correctly (#788).
- Terraform Provider Network Mirror Protocol — NORA served only the Terraform Registry Protocol, but its own docs told users to configure
network_mirror, which speaks the separate Provider Network Mirror Protocol, so everyterraform initreturned404 "provider … not found in any of the search locations". Two mirror endpoints (GET /terraform/{hostname}/{ns}/{type}/index.jsonand…/{version}.json) are added as thin adapters over the existing registry-protocol handlers;{version}.jsonrunscheck_download(curation/blocklist parity) and namespace isolation, archive URLs route through NORA's cached/quarantined binary download, per-platform metadata is fetched concurrently, and hashes arezh:<sha256>from upstream metadata, fail-closed (a platform with no resolvable shasum is omitted, never served unhashed). The single configured upstream and mirror-mode integrity (Terraform skips origin GPG in mirror mode; NORA does not verifySHA256SUMS.sig) are documented as accepted limitations inCOMPAT.md(#801, #802).
- bcrypt
0.19.0→0.19.2(RUSTSEC-2026-0199) —bcrypt::verify()could panic on a 60-byte hash string carrying multi-byte UTF-8 at certain positions (DoS). NORA calls it inauth/htpasswd.rswith an operator-controlled hash (not wire-reachable), but the bump clears the advisory repo-wide; lockfile-only (#803). - quick-xml
<0.41DoS advisories (RUSTSEC-2026-0194/-0195) accepted — transitive viaobject_store(S3 XML parsing); noobject_storerelease resolves it yet. Exposure is low (XML comes from the operator-configured S3 backend, not attacker input), so both are ignored incargo-auditandcargo-denywith the upgrade path tracked in #799 (#800).
- Anonymous Docker pull (
auth.docker_anon_pull,NORA_AUTH_DOCKER_ANON_PULL) — a dedicated, default-off switch that servesdocker pullwithoutdocker login. With auth enabled, an anonymousGET /v2/returns a401Basic challenge (sodocker loginworks); underanonymous_read = truethe manifest/blob reads themselves were served anonymously, but the/v2/ping still challenged. Whether a logged-outdocker pullthen succeeded depended on the client's image store: Docker's containerd image store tolerated the/v2/challenge and pulled anonymously, while the classic docker/distribution store cached the Basic challenge and aborted withno basic auth credentials(#778). Whendocker_anon_pull = true, the/v2/ping returns200so anonymous pull works uniformly for both stores, and manifest/blob/tag reads are served without auth; writes (push/delete) still require a token,/v2/_catalogstays authenticated (no anonymous repository enumeration), and a request that carries anAuthorizationheader is still validated (sodocker login -u token -p <nra_…>and audit attribution keep working). The switch is independent ofanonymous_read, so serving Maven/raw/npm anonymously never exposes container images. Behavior change: anonymous access to Docker/v2read endpoints is now governed solely bydocker_anon_pull. Deployments that pulled images anonymously underanonymous_read = true(containerd image store) must setdocker_anon_pull = trueto keep that working. Clients built oncontainers/image(skopeo/podman/buildah) read auth parameters only from the/v2/ping, so their authenticated operations degrade while the switch is on — keep it off if you need both anonymous pull and authenticated operations for those clients (#778). - Upstream circuit-breaker state in
/health— the/healthresponse gains anupstreamssection, one entry per enabled proxy registry, so operators without Prometheus/Grafana can see which upstreams are reachable (previously this was only on thenora_circuit_breaker_stategauge). Each entry reportsstatus—closed/open/half_open(mirroring the gauge labels), ordisabledwhen the circuit-breaker feature is off (the default) — plusfailure_countandlast_failure_seconds_ago. The state is read from the breaker's cached in-memory snapshot, so/healthnever performs a live upstream probe and stays fast and non-blocking; an enabled registry with no recorded breaker yet defaults to a healthyclosed. The OpenAPIHealthResponseschema is updated to match (#773).
- A present-but-empty
[<registry>]table now keeps the default upstream — npm/pypiproxyand maven/dockerproxies/upstreamsused a bare#[serde(default)]that deserialized toNone/[], diverging from theDefaultimpl's real upstream. Writing[npm](or[pypi]/[maven]/[docker]) inconfig.tomlto set, say, a timeout — without restating the proxy key — silently disabled proxying for that registry, while omitting the table entirely kept the upstream. The serde field-default is now single-sourced with theDefaultimpl, and a guard test asserts this for every registry section so the class cannot recur. Behavior change: if you relied on a present-but-proxy-less table to run a registry local-only (air-gapped), set the proxy env var to empty instead —NORA_NPM_PROXY="",NORA_PYPI_PROXY="",NORA_MAVEN_PROXIES="",NORA_DOCKER_PROXIES="". - Docker is now counted in the proxy and quarantine config guards — two hand-rolled per-registry checks in config validation (the
min_release_age-needs-quarantine guard and the "any quarantine active" check) enumerated registries by hand and omitted Docker. A Docker-only proxy withmin_release_ageand no quarantine was not flagged, and a Docker-only[curation.docker] quarantinewas validated incorrectly. Both now derive from a single compiler-exhaustive match over the registry set, so no registry can be silently dropped again (#765). cargo publishagainst the Cargo registry no longer 404s — the sparse-indexconfig.jsonadvertised itsapibase as{base}/cargo/api. Cargo appends/api/v1/...to that base, so publish requests went to/cargo/api/api/v1/crates/newand returned404. The advertisedapibase is now the registry mount ({base}/cargo), so Cargo builds/cargo/api/v1/crates/newand resolves to the mounted route; tests cover both the metadata and publish routes derived from theconfig.jsonapi base (#783).
- A per-registry-only quarantine now loads its durable store — the digest-quarantine store was loaded only when the global
curation.quarantinewas set. A Docker-only[curation.docker] quarantinegot an empty (non-durable) store: after a restart the on-disk first-seen records were ignored, so a still-young, already-cached digest was served before its hold expired. The store now loads whenever any quarantine — global or per-registry — is active (#765). Behavior change: an explicit globalcuration.quarantine = "off"no longer loads the store (it has no effect to enforce); set a real mode (observe/enforce) where you want enforcement.
- Index rebuild drops the per-key
stat()— rebuild walkedstorage.list()and then issued a separatestorage.stat()per key for size/mtime; on S3 thatstat()is a HEAD, so rebuilding N objects cost 1 LIST + N HEADs, all under the per-registry rebuild lock — the first reader after an invalidation blocked for the whole serialized round-trip. A new additiveStorageBackend::list_with_meta()reuses the size/mtime the directory walk (local) or LIST response (S3) already carries, so the rebuild pays zero extra HEADs. The default trait impl falls back tolist()+ per-keystat(), so every other backend stays correct, and gc/retention/backup/mirror keep usinglist()unchanged; a counting-backend test asserts the rebuild makes zero per-keystat()calls (#759).
- First-seen digest-quarantine generalized to every proxy registry — the unspoofable first-seen cooldown (previously Docker-only) now guards all 11 proxy registries (npm, PyPI, Cargo, Go, Maven, RubyGems, NuGet, Conan, pub.dev, Terraform, Ansible).
min_release_ageon a proxy path now defers to quarantine, because upstream publish dates are unsigned and several registries expose none. Breaking: enablingmin_release_ageon an enabled proxy now requires an active quarantine (orserver.trust_upstream_dateswhere a real upstream date is available); a min-age-only proxy policy is rejected at startup (#741, #742). - Release-age freshness honored with
trust_upstream_dates(#748) — under #742,min_release_agedefers to quarantine, which holds on NORA's own clock, so a provably-old artifact was held as "new to this mirror" regardless of its release date. Whenserver.trust_upstream_datesis set and the registry supplies a date, the quarantine now seeds first-seen from the trusted upstream release date — an artifact older than the TTL matures immediately and is served, while a fresh one is still held. Wired for every dated proxy registry: PyPI (PEP 691 / PEP 700upload-time), npm, Cargo, Go, NuGet, Conan, pub.dev, Maven (Central search API), RubyGems (v1 versions API), Ansible (Galaxycreated_at) and Terraform (registry.terraform.io/v2published-at— the standard provider protocol carries no date). Each upstream-date path is gated ontrust_upstream_dates(spoofable, opt-in); hosted artifacts use cached-metadata mtime. The date is obtained on the artifact download path itself — Cargo self-primesmetadata.jsonthere (acargo buildresolves via the sparse index and never hits/api/v1/crates/{name}, so the date would otherwise never be cached), mirroring PyPI's date self-prime. Docker stays on NORA's own clock (digest-addressed, no trusted date). Internal-namespace coordinates are never sent to a hardcoded public date source (Maven Central search, registry.terraform.io/v2) — the date query is skipped for internal namespaces (#68/#733). Note: Maven, RubyGems and Terraform query their date source per download request (not cached like PyPI/Cargo/Ansible); this fires only undertrust_upstream_datesand is timeout-bounded and fail-safe — caching is a tracked follow-up. - Docker digest-quarantine bypass fixed (GHSA-4j4m-fchf-gr9r) — layer/config blobs were served on every path (
Rangeand full cache-hit, proxy-stored, proxy temp-file) and viaHEADwithout a quarantine check, and a local push could pre-mature a future upstream digest through the shared ledger key. Blob serves are now gated, andrecord_trustedwas removed so the ledger records only proxy-fetched content (CWE-693, CWE-345). - Token-management broken access control fixed (GHSA-78cx-cfhm-rgmx) — the token-management endpoints (
/ui/tokens,/api/ui/tokens, and the public/api/tokens/revoke) authenticated the caller but never authorized them, so any write-capable bearer/OIDC identity could enumerate and revoke other users' tokens (including admin and service tokens), and a read identity could enumerate them. List and revoke are now owner-scoped — a non-admin acts only on its own tokens, while admins still manage all — and a non-owned id returns404(not403) so a caller cannot probe which ids exist (CWE-862). - Admin-token self-escalation blocked (GHSA-78cx-cfhm-rgmx) — the public
POST /api/tokensroute minted whatever role was requested, so any htpasswd account could self-mint anadmintoken. Breaking: anadmintoken may now be minted via this route only by an account listed inauth.admin_users(NORA_AUTH_ADMIN_USERS), which is empty by default; read and write tokens are unaffected. If you rely on this route to create admin tokens, setNORA_AUTH_ADMIN_USERS=<your-admin-user>before upgrading (CWE-862).
- Admin storage reindex —
POST /api/v1/admin/reindex(admin-role token only) refreshes the in-memory indexes from storage so the UI reflects artifacts copied in out-of-band (rsync, Unison, BTRFS send/receive, S3 sync) without a container restart or a dummy client pull. Optional?registry=<name>scopes the rebuild to one registry (unknown names return400); the rebuild runs in the background and the call returns202 Accepted. Repeated calls are debounced (429+Retry-After). The index is process-local, so under a multi-replica deployment the call refreshes only the replica that served it — reindex each replica or roll the deployment (#735). server.trust_upstream_dates— opt-in flag that letsmin_release_ageuse a real upstream publish date where one is cached (e.g. npmtime), as an enhancement to — not a substitute for — quarantine (#729).- npm
/-/whoamiendpoint — token-based identity sonpm whoamiresolves against a NORA token (#720). auth.admin_users(NORA_AUTH_ADMIN_USERS) — a comma-separated list of htpasswd usernames permitted to mintadmin-role tokens viaPOST /api/tokens; the bootstrap for admin designation (GHSA-78cx-cfhm-rgmx).
- Index rebuild no longer caches a failed storage scan as a fresh empty result — if the storage listing errored mid-rebuild, the index was cached empty and clean, so the UI could report zero artifacts on healthy data until the next write. A failed scan now leaves the index dirty and retries on the next read (#735).
- Partial
config.toml— missing[server],[storage], or fields likehost/portno longer prevent startup; serde defaults are applied for all unset values. - Container image no longer overrides
config.toml— the image shipped config values (NORA_PUBLIC_URL,NORA_PORT,NORA_STORAGE_PATH,NORA_AUTH_TOKEN_STORAGE) as bakedENV, which silently won over a user-providedconfig.toml(env has the highest precedence inConfig::load). Defaults now ship as a file (/etc/nora/config.toml, loaded viaNORA_CONFIG_PATH); a bind-mountedconfig.tomltakes full effect. OnlyNORA_HOSTstays inENVso binding survives a partial mounted config and the container stays reachable (#719). - Namespace isolation now covers every proxy registry's metadata path —
internal_namespaces(the dependency-confusion defense, always active) previously gated only the download/tarball path, so a metadata / index / version-list / search request for an internal-namespace package leaked its name upstream on every proxy registry except npm. The guard now runs on the metadata path of PyPI, Cargo, Maven, Go, NuGet, Conan, pub.dev, Terraform, Ansible and RubyGems — and on the NuGet/Conan search query — serving any locally-published or cached copy first and blocking only the genuine upstream fetch (no leak, and no false 403 on a locally-published internal package). The npm TTL-stale metadata refetch is also guarded, closing a residual of #725 (contrib-kit#68). - Locally-published internal packages are served instead of being blocked —
internal_namespacesis documented as "never proxied upstream", butcheck_downloadran the always-on namespace filter before the local serve, so a mixed proxy+host instance returned 403 for its own internal packages on every download path (npm, PyPI, Cargo, Maven, Conan, RubyGems, NuGet, pub.dev, Go, Ansible, Docker, raw) and on the NuGetregistration_index, pub.devpackage_listingand RubyGemscompact_indexmetadata paths. An internal name now serves any local/cached copy first and blocks only the genuine upstream fetch; an internal name with no local copy is still blocked and never proxied. Non-internal behavior is unchanged (#733). - Enforce mode requires at least one active control —
curation.mode = enforceno longer hard-requiresallowlist_path; a blocklist-only, min-release-age-only, or quarantine-only policy is valid, and enforce is rejected only when no control of any kind is configured (#740). - Basic-auth accepts an API token as the password — clients sending an API token over HTTP Basic auth (
user:<token>) are now authenticated, matching the token-in-header behavior (#737). - Crash durability — the parent directory is
fsync'd after the atomic rename, so a published artifact survives a power loss immediately after write (#723). - npm scoped-package publish — scoped attachment filenames (
@scope/name) are normalized, so the tarball is stored and served under the correct key (#724). - npm
whoamiresponse — serialized viaserde_jsoninstead offormat!, avoiding malformed output on unusual usernames (#722).
- Multiple PyPI upstream proxies —
NORA_PYPI_PROXIES(or[pypi].proxies) configures an ordered list of upstreams. The order is the precedence — the first upstream that lists or serves a file wins, like pip's--index-urlahead of--extra-index-url; locally cached/uploaded files win over all upstreams. The mount-points table in the UI lists every configured upstream (#663, #706). - Dual-stack IPv4+IPv6 bind — the
::wildcard now accepts both address families (IPV6_V6ONLYcleared via socket2), with a0.0.0.0fallback when IPv6 is unavailable, so the default container bind serves both (#696). - Docker OCI single-POST monolithic blob upload —
POST /v2/<name>/blobs/uploads/?digest=...is now supported per the OCI Distribution spec (#698). - Docker Range requests for blob GET —
Range/206 Partial Contentenables resumable image pulls (#657). nora healthcheckCLI subcommand — a dependency-free loopback probe for a DockerHEALTHCHECK; it ignoresHTTP_PROXYand probes IPv4 loopback so it reaches a wildcard or0.0.0.0bind (#695, #701).- Compile-time integrity witnesses (typestate pilot) — served artifacts carry a type-level proof that their hash-pin was discharged at the serve site; rolled out to the buffered-serve path (#666, #674).
- Conditional-request revalidation for mutable/stale metadata — Docker tags, the Cargo sparse index, Maven metadata, Go version listings, npm packuments, and Ansible / Gems / Conan / NuGet / Pub package metadata now revalidate against upstream (
If-None-Match/ TTL) before serving from cache instead of serving blindly stale (#639, #641, #643, #646, #647, #669, #670, #671, #672, #673). - Single-flight upstream coalescing — concurrent cache-miss fetches for the same artifact collapse into one upstream request (#618); npm metadata revalidates with
If-None-Matchon TTL expiry (#617). - Per-registry observability — per-registry artifact and storage gauges plus process uptime (#637), and curation allow/block decisions exposed via Prometheus (#636).
- Configurable token-verify cache TTL (
NORA_AUTH_TOKEN_CACHE_TTL) — bounds the cross-replica token-revocation window (#668). - Operator re-pin recovery — a CLI path to re-pin integrity-failed artifacts after the operator verifies them (#620).
- Startup safety warnings — NORA warns loudly when running without authentication (#635) and when
public_urlis unset on a loopback bind (#591). - Docker
default_action = deny— reject image names that match no configured upstream rule (#572).
- Dashboard counters are served from the Prometheus registry instead of a separately-persisted
metrics.json— the on-disk copy and its periodic write are gone, so the UI and/metricscan no longer disagree, and the figures are "since restart" (shown via a hover tooltip on the affected stat cards) (#626, #703, #706). - Streamed Docker blob downloads no longer buffer the full blob in RAM (#580, #589).
serve-stalebehavior is aligned across all registry handlers (#576, #577).- Client-facing URL construction (service-index rewriting, UI install commands,
docker pull) is centralized inServerConfig::public_base_url()/public_host(), replacing three divergent inline copies (#594). - Instrumented the buffered
get()integrity-verify cost (nora_storage_verify_duration_seconds) for capacity planning (#619).
- Dashboard / UI — the sidebar nav lists only enabled registries instead of all formats (#704, #705); real on-disk dashboard stats instead of virtual/double-counted figures (#621); search added to the Maven/Go browsers to match the list-page contract (#622).
- Reverse-proxy sub-path mounts — UI self-links, static assets, inline
fetchcalls, redirectLocationheaders, and the API-docs / Swagger URLs are now prefixed with the path component ofpublic_url; root-vhost deploys are unaffected (the prefix is empty, a no-op) (#685, #686, #690). The UIdocker pullcommand uses the bare host authority, and the IPv6 fallback base URL brackets the address (http://[::1]:4000). - PyPI — percent-encoded filenames (e.g.
+cuXXXwheels published as%2B) now match when proxying a custom index, instead of 404ing (#699). - Docker — deleting a manifest by digest also removes tags that resolve to it (#697); manifest blob references are validated and tag writes serialized on push (#656); upload temp files orphaned by a write failure are swept on the periodic sweep, not only at boot (#683, #684); the release-image
HEALTHCHECKuses127.0.0.1and supports IPv6 binds (#569, #570, #573). - Cargo — the sparse-index rebuild is all-or-fail (a read error aborts instead of publishing a truncated/empty index and silently dropping versions) and regenerates from per-version entries instead of read-modify-write (#681, #682, #651).
- npm — the packument is regenerated from per-version keys instead of read-modify-write (#649).
- Storage integrity —
get()fails closed on a hash-pin mismatch (#582, #600); hash-pin writes are durable and recorded beforeput()returns (#604, #613, #633); the streaming Docker-blob serve verifies the digest while streaming and aborts on tamper (#632);health_checkwrite-probes the backing store instead of only checking the directory exists (#634). - GC — a grace period stops the collector deleting blobs belonging to in-flight pushes (#584, #611).
- Circuit breaker — a stalled half-open probe is released instead of wedging at
503(#585, #607); a 4xx probe recovers without masking real failures (#606, #614); probe reports are fenced by generation so a stale "lost" probe can't flip state (#667). - Backup — the archive is published durably via temp file +
fsync+ rename (#678). - Observability — the upstream-URL leak detector excludes NORA's own admin/UI/observability surface (
/api/,/api-docs,/ui,/health,/ready,/metrics), counting each skip asnora_leak_detection_skipped_total{reason="own_surface"}, sonora_response_upstream_url_leak_totalreflects only genuine proxy-response leaks and is alertable (#624). - Secrets — the env provider preserves
VarErrorcontext in errors (#592).
- Min-release-age quarantine now fails closed on an unknown publish date —
MinReleaseAgeFilterreturnedSkip(defer, ultimately allow) when a package's publish date could not be determined, so an artifact whose age cannot be verified bypassed the quarantine. This was the one fail-open path in an otherwise fail-closed curation engine (the config layer already rejectson_failure = "open"). An unknown date is now blocked when the quarantine is active for that registry (threshold > 0); a registry with the quarantine disabled (threshold0) still defers (#679, #680). - Curation fails closed on a malformed SIGHUP policy reload — a bad hot-reload no longer swaps in a broken engine; the active policy is kept (#586, #605).
- Mirror verifies content digests before pushing — both the manifest digest and each blob's SHA-256 are verified against the requested digest before a mirrored artifact is written (#587, #608, #609, #615).
- OIDC
namespace_scopeis now enforced on writes — it was previously parsed and documented as a per-provider access control but never applied at runtime (fail-open, #583). A provider'snamespace_scopenow restricts which artifact namespaces its tokens may publish to, across docker, raw, npm, maven, pypi and cargo. Matching is segment-aware (myorg/*matchesmyorg/repobut nevermyorg-evil/...; usemyorg/**for everything undermyorg/).- BREAKING (behavioral): if a provider's
namespace_scopeis set to anything other than["*"], out-of-scope writes from that issuer now return403. The default["*"]is unchanged and remains a no-op, so deployments that never set the field are unaffected. Check your OIDC config before upgrading. - To stage the rollout, set
namespace_scope_enforcement = "audit"on the provider: out-of-scope writes are allowed but logged and counted aswould_denyvia the newnora_auth_namespace_scope_total{provider,decision}metric. Switch to"enforce"(the default) once the metric is clean. - Scope applies to OIDC identities only; opaque (
nra_) tokens and Basic auth are unaffected. Reads are never gated.
- BREAKING (behavioral): if a provider's
- Null byte rejection middleware — new outermost layer returns 400 Bad Request for URL paths containing
\0,%00, or%2500; previously caused 500/panic in handlers (#565) - Path traversal hardening — additional guards against
../and symlink-based traversal (#560) - Rate limit inversion fix — rate limiter no longer inverts allow/deny logic in certain edge cases (#560)
- javascript: URI injection — metadata links with
javascript:scheme are now stripped (#522, #546) - Reflected XSS in install commands — UI install commands are now HTML-escaped (#521, #545)
- Invalid quarantine/curation/audit mode values rejected — fail-closed on unknown values (#524, #548)
- Credential fields migrated to ProtectedString — secrets zeroed on drop, excluded from Debug (#523, #547)
- Dependency update: tar 0.4.45 → 0.4.46 — fixes PAX header desynchronization (GHSA-3pv8-6f4r-ffg2)
- Cargo proxy User-Agent — set
nora/<version>User-Agent on the shared HTTP client; crates.io returns 403 without it (#565) - Docker TOCTOU race — upload session creation now uses atomic file operations; orphaned temp files cleaned on startup (#530, #554)
- Docker blob HEAD check — use
stat()instead of fullget()for HEAD requests; fixBytesrefcount on proxy clone (#526, #550) - npm publish with corrupt metadata — reject publish when existing metadata JSON is malformed (#533, #558)
- Terraform serve-stale — serve cached metadata when upstream is unreachable (#532, #557)
- Go Cache-Control — use
is_mutableflag instead ofcontent_typefor header selection (#531, #556) - S3 key roundtrip collision — use
%40encoding for@in S3 storage keys (#534, #559) - GC metadata serialization — serialize metadata cleanup with
publish_lock, makeput()atomic (#529, #553) - StorageBackend::list() — now returns
Resultinstead of panicking on I/O error (#528, #552) - Auth token cache key alignment — insert and lookup use the same key format (#527, #551)
- Auth CIDR prefix=0 overflow — handle arithmetic overflow in TrustedProxies parsing (#525, #549)
- Base URL wildcard host — fail-fast on startup if host is
0.0.0.0withoutNORA_PUBLIC_URL(#510, #511, #512) - Metrics body size_hint — leak detection guard uses
size_hintinstead ofcontent_length(#517, #519)
- Config refactor —
config.rssplit into per-registry config modules for maintainability (#484, #564) - AppState Clone —
AppStatenow implementsClonefor AxumFromRefdecomposition (#483, #516) - Proxy fetch newtypes — replaced stringly-typed proxy parameters with newtypes (#482, #515)
- LazyLock migration — replaced
lazy_static!withstd::sync::LazyLock(#373, #480, #514) - LOCK-SAFE annotations — all cache-through proxy functions annotated with lock safety guarantees (#518, #520)
- Rust toolchain pinned to 1.96.0 (#555)
- Playwright E2E contract tests — typed contracts for all 13 registry UI pages, visual regression screenshots (#565)
- 1204 tests (up from 1086 in v0.9.2)
NORA_PUBLIC_URLrequired whenhost=0.0.0.0— prevents misconfigured URL rewriting. SetNORA_PUBLIC_URL=https://your-domain.comin your environment. (#510, #512)
- Prometheus P0 metrics —
nora_downloads_total,nora_uploads_total,nora_storage_bytes,nora_cache_requests_total,nora_upstream_request_duration_secondshistogram with per-registry labels (#431, #432, #443) - Grafana dashboard — production-ready dashboard JSON in
dist/grafana-dashboard.jsonwith documentation (#436, #437) - Ansible Galaxy v3 compliance — pagination forwarding, artifact route alias, spec name validation (#433, #434, #438, #444, #445)
- .deb/.rpm packaging —
nfpmconfiguration for native Linux packages (#209, #435) - Circuit breaker gauge initialization —
nora_circuit_breaker_stateemits 0 (CLOSED) at startup for all enabled registries (#441) - PyPI URL-rewrite tests — 11 tests covering trailing-slash and double-slash regressions (#387)
- 1086 total tests (up from 1049)
- npm upstream URL leak (P0 security) — metadata responses no longer expose
registry.npmjs.orgURLs (#439) - Cargo sparse index
apifield —config.jsonnow returns correct/cargo/apipath instead of/cargo(#442) - PyPI trailing-slash URL rewrite — response body URLs no longer contain double-slash
//simple(#387)
- Dashboard screenshot updated to v0.9.2 with populated metrics panels (#429, #430)
- README and SECURITY.md synced with v0.9.2 (#428)
- NuGet gzip registration —
RegistrationsBaseUrl/3.6.0responses compressed with gzip per NuGet V3 spec (#421) - NuGet semVerLevel filtering — search and autocomplete hide SemVer 2.0 packages when
semVerLevelnot specified (#421) - NuGet service index generation — generate service index from scratch instead of rewriting upstream, ensures all
@idURLs point to Nora (#404, #405) - NuGet Chocolatey/PowerShell aliases —
/chocolatey/and/powershell/path aliases for NuGet V3 endpoints (#412, #419) - NuGet local autocomplete fallback — autocomplete works in air-gap mode using cached package index (#414, #417)
- NuGet serve-stale — serve cached metadata when upstream is unreachable, with
X-Nora-Staleheader (#409, #410, #411) - NuGet deprecation/vulnerability pass-through — registration responses preserve deprecation and vulnerability metadata from upstream (#425)
- Cargo ETag + HTTP 304 — sparse index responses include SHA-256 ETag;
If-None-Matchreturns 304 Not Modified (#397) - Upstream URL leak detection metric — Prometheus counter
nora_upstream_url_leak_total{registry, leak_type}fires when response bodies/headers contain upstream registry URLs (#386, #426) - NuGet E2E test suite — 11 dotnet client fixture projects covering restore, analyzers, source generators, native RID, SemVer2, version ranges, case insensitivity, lock files, deep transitive deps, and Chocolatey alias
- NuGet URL rewriting — registration index/page
@idandpackageContentURLs no longer leakapi.nuget.org(#388, #392, #393, #394, #400) - NuGet background fetch — index fetch routed through
proxy_fetch_textto respect proxy and circuit breaker settings (#413, #416) - NuGet upstream URL stripping — strip path component from upstream proxy URL to prevent double-path (#407, #408)
- NuGet serve_stale config — respect
serve_staleconfig flag in search/autocomplete fallback (#423) - PyPI PEP 691 typed structs — replaced ad-hoc JSON manipulation with typed Serde structs for spec conformance (#390, #398)
- PyPI file hash key — renamed
digeststohashesto support PEP 691 specification (#389, #399) - npm scoped package tarball key — correct tarball storage key for
@scope/packagein UI detail view (#402, #403) - Air-gap URL leaks — fixed upstream URL leaks across NuGet, Terraform, and Ansible registries (#400)
- Curation test serialization — serialize env-override tests with mutex to prevent flaky parallel failures (#406)
- NuGet search endpoint discovery — dynamically discover search/autocomplete endpoints from upstream service index instead of hardcoding (#370, #418)
- NuGet metadata proxy timeout — reduced from default to 2s for faster fallback to cache (#415, #420)
- URL-leak invariant tests — added URL-leak detection tests for NuGet and npm registries (#390, #395)
- 1049 total tests (up from 994)
- OIDC / Workload Identity — zero-secret auth for GitHub Actions and GitLab CI JWT tokens (#342)
- Cache-Control completeness — extend caching headers to all remaining registries (#340)
- Docker streaming blob uploads — chunked upload processing eliminates OOM on large images (#368)
- Docker path-based upstream routing — route pulls to specific upstreams by image path prefix (#365)
- Docker metadata TTL + stale-while-error — cached manifests revalidate against upstream after configurable TTL; serve stale on upstream failure (#311)
- Docker/OCI mirror namespacing — per-upstream namespace prefix isolates storage keys, with lazy migration from legacy flat layout (#323)
- Per-registry circuit breaker overrides —
[circuit_breaker.overrides."registry:url"]allows custom thresholds per upstream (#339) - Streaming read_timeout for Docker blobs — per-chunk timeout prevents stuck connections on large layer downloads (#341)
- Hot reload for curation policy — SIGHUP reloads blocklist/allowlist without restart using lock-free ArcSwap (#343)
- linux/arm64 support — multi-platform Docker images and binary releases for ARM64 (#193)
- Production deployment files —
deploy/docker-compose.prod.ymlanddeploy/nora.servicesystemd unit (#307)
- Manifest response builder — extracted
manifest_response()helper, removing 3 duplicate return paths in Docker registry (#338) - Env var naming convention — shortened variables to
NORA_{SECTION}_{FIELD}pattern (under 30 chars), e.g.NORA_TF_*,NORA_CURATION_INTERNAL_NS
- Add Content-Length header to
library/fallback manifest response (#337) - Docker 3+ path segments (
org/team/app) routed correctly (#309) - GC blob ordering — blobs deleted before manifests to prevent dangling references (#305)
- GC graceful SIGTERM — flush pending deletions on shutdown (#306)
- AuditLog singleton — single instance instead of duplicate per registry (#308)
- UI mount points table shows all configured upstreams (#312)
- Token owner set to real authenticated user instead of "admin" (#322)
- Race conditions, non-atomic writes, and version sorting (#318, #334)
- Log storage write failures instead of silently discarding (#317, #332)
- Security hardening — health endpoint sanitization, auth warning, Docker realm validation (#330)
- Security hardening — XSS protection, injection prevention, input validation (#319, #335)
- Raw registry Cache-Control changed from
immutableto configurableno-cachedefault (#302, #329) - NuGet: use shared http_client for flatcontainer index fetch (#331)
- Catch panics in background cache tasks, consolidate Go registry spawns (#333)
- Log audit write and serialization failures instead of swallowing (#321, #327)
- Write
.cratetarball before sparse index to prevent zombie versions (#316, #328) - Move blocking file I/O out of upload session lock scope (#313, #326)
- Use proxy-aware client IP in token API rate limiting (#314, #325)
- Flush token
last_usedon graceful shutdown (#304, #324)
- README and ROADMAP synced with current state (#344)
- Configuration reference updated with raw
cache_controldocs (#303)
- Outbound HTTP/SOCKS5 proxy support (#296)
- Structured audit log with configurable output (#286)
- Raw registry RFC 9110 conditional PUT (#278)
- Raw registry POST /raw/-/reindex endpoint (#276)
- Reverse proxy setup guide (#275)
- Duplicate library/ prefix block in Docker download_blob (#297, #285)
- Security hardening: HTML escape, brute-force, realm validation (#292)
- Warn-level log when all proxy upstreams fail (#284)
- Log all silent storage and proxy errors (#282)
- PyPI: merge upstream and local files in simple index (#295)
- Flaky quarantine persistence test under tarpaulin (#299)
- OpenAPI 429 docs, 405 with Allow header (#279)
- 994 total tests (up from 910)
- TTL race condition — unified TTL semantics across registries; repo_index invalidation no longer races with concurrent publishes (#266)
- NuGet autocomplete leak —
SearchAutocompleteServiceURLs in service index now rewrite to NORA instead of leaking toazuresearch-*.nuget.org. New/nuget/v3/autocompleteproxy endpoint with graceful fallback (#262) - NuGet gallery leak —
SearchGalleryQueryServiceroot URLs (azuresearch-{usnc,ussc}.nuget.org/) now rewrite to NORA. Zero azuresearch URLs remain in service index - NuGet 429 during cache warming — registry proxy routes no longer double-limited by
general_limiter+upload_limiter. Removes 429 errors duringdotnet restorewith many packages while keeping auth rate limiting active - E2E test paths — NuGet smoke tests used wrong paths (
/v3/flat/→/v3/flatcontainer/,/v3/search→/v3/query)
- NuGet search fallback — local search from repo index when upstream is unavailable, download tracking for proxied packages (#261)
- Env var naming guideline —
CONTRIBUTING.mddocumentsNORA_{SECTION}_{FIELD}pattern with abbreviation convention (NORA_CB_*) - 910 total tests (up from 909)
- Docker base images switched to real RED OS and Astra Linux images (#260)
- NuGet autocomplete config: env var
NORA_NUGET_AUTOCOMPLETE, config fieldautocomplete
- UI polish — improved dashboard layout and proxy index reliability
- Error logging — better error messages for proxy failures (#259)
- Hash Pin Store — content-addressable integrity verification for all stored artifacts,
put_if_absent()semantics with NDJSON persistence (#229) - Trusted proxy support —
NORA_AUTH_TRUSTED_PROXIESaccepts CIDR ranges for X-Forwarded-For extraction (#230) - Cache-Control headers — proper caching directives for proxy registries: Docker, Maven, npm, Cargo, PyPI, Go, Pub, Raw (#230)
- Auth rate limiting — per-IP exponential backoff on failed authentication (429+Retry-After) (#229)
- Docker publish_locks eviction — automatic cleanup of stale upload locks (#230)
- GOVERNANCE.md and ROADMAP.md — project governance model and public roadmap (#228)
- Version consistency gate —
scripts/pre-commit-check.shvalidates Cargo.toml vs OpenAPI vs Cargo.lock versions, enforced in release pipeline (#224, #225) - 908 total tests (up from 851)
- Docker proxy timeout — default timeout raised from 60s/120s to 300s, large image pulls no longer time out (#233)
- Unicode path validation — non-ASCII characters in Maven/Raw upload paths now return 400 instead of 500 (#234)
- Docker /v2/ auth — require authentication per Docker V2 spec (#220)
- Curation bypass token timing — constant-time comparison using
subtlecrate (#230) - S3 paginated listing — storage size calculation now handles >1000 objects correctly (#230)
- Docker temp file cleanup — upload temp files are removed on failure (#230)
- OpenAPI schema deduplication — removed 8 duplicate type definitions (#227)
- OpenAPI status codes — documented 400/409/413/422/503 responses that API already returns (#235)
- Mobile-responsive UI — dashboard grid, hidden table columns on small screens, Raw registry "Files" tab (#218)
- Startup metric renamed to
startup_duration_mswith Cold Start display on dashboard (#218) - Guardrails: semver-checks, Renovate config, pre-commit hooks, clippy deny rules (#225)
- cargo-deny-action bumped to v2.0.17 (#231)
- Rate limiting hardening for token endpoints (#229)
- Curation completeness checks for all registry formats (#230)
- Raw registry glob pattern validation (#230)
- Docker /v2/ auth flow — endpoint now correctly returns 401 Unauthorized with WWW-Authenticate header when auth is enabled. Previously, Docker clients received 200 OK without authentication, causing
docker loginto appear successful whiledocker pull/docker pushfailed with "unauthorized" (#219) - Raw registry curation bypass — raw was the only registry without
check_download(), completely bypassing curation enforce mode. All 13 registries are now curated consistently - Timing side-channel on bypass token — replaced string comparison with constant-time comparison (
subtlecrate) to prevent timing attacks - Maven glob matching —
com.evil.**pattern now correctly matchescom.evil:lib(colon separator for Maven groupId:artifactId) - Mobile dashboard — responsive layout with 3-column stats grid, compact padding, and word-wrap on small screens
- Raw directory browser — nested navigation with breadcrumbs, folder/file icons, directories-first sorting. Browse raw artifacts at any depth
- Docker Hub images — NORA is now published to Docker Hub as
getnora/noraalongside GHCR - Docker-Distribution-API-Version header —
/v2/response now includesregistry/2.0header per Docker Registry V2 spec - Startup time metric —
startup_duration_msexposed on dashboard (cold start tracking) - 857 tests (up from 851)
- Publish date extraction — curation min-release-age filter now extracts real publish dates from cached metadata for npm, PyPI, Cargo, and Go registries (#207)
- Per-registry curation overrides — configure min_release_age per registry via TOML (
[curation.npm] min_release_age = "3d") or env (NORA_CURATION_NPM_MIN_RELEASE_AGE) (#205) parse_iso8601_to_unix()helper for ISO 8601 / RFC 3339 date parsing across registry formats
- Raw registry: UI now updates immediately after upload/delete — added missing
repo_index.invalidate("raw")calls (#212)
- Token RBAC:
last_usedtracking (deferred flush), auto-expire rejection, description field — all functional (#206)
- Min-release-age filter — block packages younger than N days/hours/weeks (#132). Config:
min_release_age = "7d", envNORA_CURATION_MIN_RELEASE_AGE - Token RBAC — read/write/admin roles per token, expiry badges in UI, expired tokens sorted to bottom (#124)
- Dynamic stats footer — demo builds show live binary size, VmRSS, registry count from /proc (replaces hardcoded values)
- 850 total tests (up from 821)
- Token list UI: expired tokens show red badge, sorted to bottom with reduced opacity
format_expiry()replacesformat_timestamp()for token expiry display — correctly shows "in 28d" for future, "expired 3d ago" for past#[non_exhaustive]onRoleenum for forward compatibility
- Declarative registry selection —
[registries] enable = ["docker","npm"]/"all"/["all","-maven"], envNORA_REGISTRIES_ENABLE, 3-tier priority (env > TOML > legacy) - Curation layer — policy engine for download filtering across all 13 registries (#184-#190)
- Blocklist/allowlist rules with glob patterns and namespace isolation
- Three modes:
off(passthrough),audit(log only),enforce(block downloads) - Integrity verification via SHA256/SHA512 checksums
- CVE blocking via blocklist rules (manual CVE entries)
- CLI tools:
nora curation validate,nora curation explain
- RubyGems proxy registry (
/gems/) — compact index, gem/gemspec immutable caching, TTL-based index refresh (#141) - Terraform proxy registry (
/terraform/) — provider/module proxy with service discovery, download_url rewriting (#133) - Ansible Galaxy proxy registry (
/ansible/) — Galaxy v3 API, collection tarball immutable caching (#134) - NuGet v3 proxy registry (
/nuget/) — service index @id URL rewriting, .nupkg/.nuspec immutable caching (#140) - Pub (Dart/Flutter) proxy registry (
/pub/) — package metadata URL rewriting, SHA256-verified archive caching (#166, based on PR #191 by @mit-73) - Conan V2 proxy registry (
/conan/) — recipe/package caching with immutable revision-scoped storage, ConanCenter upstream (#142) - Dynamic registry loading — only enabled registries mount routes, appear in UI sidebar and health endpoint
- Per-registry
enabledflag in config (env:NORA_DOCKER_ENABLED,NORA_MAVEN_ENABLED, etc.) - Shared
RegistryTypeenum for type-safe cross-module registry identification - UI: 13-registry sidebar with format-specific SVG icons, dashboard cards for all registries
- Short-SHA Docker tags in CI builds (#182, #192)
- Copyright updated to "The NORA Authors"
- OpenAPI spec version synced with Cargo.toml
- UI install commands now respect
NORA_PUBLIC_URLfor all registries — PyPI, npm, Go, Raw, Docker (#177) - Docker
WWW-Authenticaterealm usesNORA_PUBLIC_URLinstead of hardcoded "Nora" (#177) - PyPI simple index generates absolute download URLs using
NORA_PUBLIC_URL(#177)
- S3 storage mode: removed Dockerfile ENV override that forced local mode regardless of config.toml (#173)
- Audit log and dashboard metrics: create parent directories before file open (fixes crash with readOnlyRootFilesystem)
- Security: update rustls-webpki to 0.103.13 (RUSTSEC-2026-0104)
- GC and Retention schedulers now share a cleanup lock preventing concurrent
storage.delete()races (#164) - Publish lock race conditions: Maven lock guard was inside if-block (P0), Cargo lock key was per-version instead of per-crate (P1), Docker pull counter lacked lock (P2) (#160)
- Raw registry enforces immutability — overwrites return 409 Conflict instead of silently replacing files (#162)
- Retention
dry_run=truevalidation warning added (symmetric with GC) (#162) - Flaky test:
validate()read env var directly, parallel tests broke each other (#160) llms.txtmirror CLI examples corrected:--image→--images,--package→--packages, pip/cargo/maven use--lockfile(#161)
- OpenAPI spec expanded: npm publish, Cargo publish, PyPI upload, Cargo sparse index, Docker manifest delete endpoints documented (#161, #163)
- README env var table expanded from 10 to 24 variables with full descriptions (#163)
- README mirror subcommand examples added for all 6 formats (#163)
- Maven auth column corrected from "proxy-only" to full auth support (#163)
- Coherence CI pipeline added: version sync, env var coverage, registry list, dead code budget, license check (#156)
- Negative integration tests added for auth and validation (#156)
- Config validation warns on Docker proxy credentials in env var (#157)
- Config validation warns on relative paths with explicit config (#154)
- Maven env var overrides added, S3 default port fixed to 9000 (#153)
- Docker pull counter added with publish lock (#160)
lock-audit.shscript and Makefile targets added (#160)- 633 total tests (up from 588)
- Upgrade Alpine 3.20 → 3.21, patching 18 CVEs (5 HIGH: OpenSSL, musl, zlib-ng)
- ArtifactHub logo added to Helm chart metadata
- Helm chart support —
helm repo add nora https://getnora-io.github.io/helm-charts
- README updated for v0.6.0
- Maven registry — immutable releases with publish mutex, checksum generation (MD5, SHA-1, SHA-256, SHA-512),
maven-metadata.xmlauto-generation - Retention policies —
keep_last,older_than_days,excludepatterns per registry;retention-plan(dry-run) andretention-apply --yes(safe-by-default) - Background retention scheduler —
retention.enabled = truewith configurable interval, single-flight lock prevents overlapping runs - Retention Prometheus metrics —
nora_retention_versions_deleted_total,nora_retention_bytes_freed_total,nora_retention_duration_seconds,nora_retention_last_run_timestamp - GC expanded to all registries — Go incomplete version detection (missing
.infoor.zip), Cargo index/crate cross-check, Maven/npm/PyPI checksum orphans, Docker blob orphans - GC/Retention visibility — reports uncovered registries with file counts after each run
- Go retention collector —
keep_lastfor Go modules, parsingmodule/@v/version.{info,mod,zip} - Audit log — one entry per retention run with keys/bytes/duration
- 588 total tests (up from 577)
- GC now requires
--applyflag to delete (dry-run by default) - Retention requires
--yesto apply (plan-only by default) - Binary size reduced from 60MB to 21MB (stripped debug symbols in release profile)
RetentionConfigexpanded withenabled,intervalfields and env var overrides (NORA_RETENTION_ENABLED,NORA_RETENTION_INTERVAL)
md-5crate aligned to0.11(compatible withdigest 0.11), replacingmd5 0.7which lackedDigesttrait- Clippy warnings cleaned up across all modules
dead_codewarning onArtifactMetasuppressed- Token sorting uses
sort_by_keyfor stability
- Cargo sparse index (RFC 2789) — cargo can now use NORA as a proper registry with
sparse+http://protocol, includingconfig.json, prefix-based index lookup, andcargo publishwire format support - Cargo publish — full publish flow with wire format parsing, version immutability (409 Conflict), SHA-256 checksums in sparse index, and proper
warningsresponse format - PyPI twine upload —
twine uploadvia multipart/form-data with SHA-256 verification, filename validation, and version immutability - PEP 691 JSON API — content negotiation via
Accept: application/vnd.pypi.simple.v1+jsonfor package index and version listing, with hash digests in responses - 577 total tests (up from 504), including 25 new Cargo tests and 18 new PyPI tests
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Cargo dependency field mapping:
version_reqcorrectly renamed toreqandexplicit_name_in_tomltopackagein sparse index entries, matching Cargo registry specification - Cargo crate names normalized to lowercase across all endpoints (publish, download, metadata, sparse index) for consistent storage keys
- Cargo publish write ordering: index written before .crate tarball to prevent orphaned files on partial failure
- Cargo conflict errors now return Cargo-compatible JSON format (
{"errors": [{"detail": "..."}]}) - PyPI hash fragments preserved when rewriting upstream links (PEP 503 compliance)
- Redundant path traversal checks removed from crate name validation (charset already excludes unsafe characters)
- Cargo sparse index and config.json responses include
Cache-Control: public, max-age=300 - Cargo .crate downloads include
Cache-Control: public, max-age=31536000, immutableandContent-Type: application/x-tar - axum upgraded with
multipartfeature for PyPI upload support
- Docker image mirroring — nora mirror docker fetches manifests and blobs from upstream registries (Docker Hub, ghcr.io, etc.) and pushes into NORA (#41)
- yarn.lock support — nora mirror yarn parses v1 format with scoped packages and dedup (#44)
- --json output for mirror — nora mirror npm --json outputs structured JSON for CI/CD pipelines (#43)
- Storage size in /health — total_size_bytes field in health endpoint response (#42)
- 499 total tests (up from 466), 61.5% code coverage (up from 43%)
- fetch_blob_from_upstream and fetch_manifest_from_upstream are now pub for reuse in mirror module
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - tarpaulin exclude-files paths corrected to workspace-relative (coverage jumped from 29% to 61%) (#92)
- Env var naming unified across all registries (#39, #90)
- Token verification cache — in-memory with 5min TTL, eliminates repeated Argon2id on every request
- Property-based tests (proptest) for Docker/OCI manifest parsers (#84)
- 466 total tests, 43% code coverage (up from 22%) (#87)
- MSRV declared in Cargo.toml (#84)
- Upload sessions moved from global static to AppState
- Blocking I/O replaced with async in hot paths
- Production docker-compose includes Caddy reverse proxy
- clippy.toml added for consistent lint rules
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Proxy request deduplication — concurrent requests coalesced (#83)
- Multi-registry GC now handles all 7 registry types (#83)
- TOCTOU race condition in credential validation (#83)
- Config validation at startup — fail fast with clear errors (#73)
- Raw registry in dashboard sidebar, footer stats updated (#64)
- tarpaulin.toml config format (#88)
- sha2 0.10→0.11, hmac 0.12→0.13 (#75)
- Credential hygiene — cleared from memory after use (#83)
- cosign-installer 3.8.0→4.1.1 (#71)
- Development Setup in CONTRIBUTING.md (#76)
- Roadmap consolidated into README (#65, #66)
- Helm OCI docs and logging env vars documented
- Go module proxy — full GOPROXY protocol support (list, info, mod, zip, latest) (#59)
- Upstream proxy retry with configurable timeout and backoff (#56)
- Maven proxy-only mode — proxy Maven artifacts without local storage (#56)
- Anonymous read mode docs — Go proxy section in README (#62)
- Integration tests: Docker push/pull, npm install, upstream timeout (#57)
- Go proxy and Raw registry integration tests in smoke suite (#72)
- Config validation at startup — clear errors instead of runtime panics
- Dockerfile HEALTHCHECK for standalone deployments (#72)
- rust-toolchain.toml for reproducible builds (#72)
- Token hashing migrated from SHA-256 to Argon2id — existing tokens auto-migrate on first use (#55)
- UI: Raw registry in sidebar, footer stats updated (32MB, 7 registries) (#64)
- README restructured: roadmap in README, removed stale ROADMAP.md (#65, #66)
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Remove all unwrap() from production code — proper error handling throughout (#72)
- Add
#![forbid(unsafe_code)]— no unsafe code allowed at crate level (#72) - Add input validation to Cargo registry endpoints (#72)
- Improve expect() messages with descriptive context (#72)
- Remove 7 unnecessary clone() calls (#72)
- Restore .gitleaks.toml lost during merge (#58)
- Update SECURITY.md — add 0.3.x to supported versions (#72)
- Update rustls-webpki 0.103.9 → 0.103.10 (RUSTSEC-2026-0049)
- Argon2id token hashing replaces SHA-256 (#55)
#![forbid(unsafe_code)]enforced (#72)- Zero unwrap() in production code (#72)
- Anonymous read mode (
NORA_AUTH_ANONYMOUS_READ=true): allow pull/download without credentials while requiring auth for push. Use case: public demo registries, read-only mirrors.
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Pin slsa-github-generator and codeql-action by SHA instead of tag
- Replace anonymous tuple with named struct in activity grouping (readability)
- Replace unwrap() with if-let pattern in activity grouping (safety)
- Add warning message on SLSA attestation failure instead of silent suppression
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - UI: Group consecutive identical activity entries — repeated cache hits show as "artifact (x4)" instead of 4 identical rows
- UI: Fix table cell padding in Mount Points and Activity tables — th/td alignment now consistent
- Security: Update tar crate 0.4.44 → 0.4.45 (CVE-2026-33055 PAX size header bypass, CVE-2026-33056 symlink chmod traversal)
- 82 new unit tests across 7 modules (activity_log, audit, config, dashboard_metrics, error, metrics, repo_index)
- Test coverage badge in README (12.55% → 21.56%)
- Dashboard GIF (EN/RU crossfade) in README
- 7 missing environment variables added to docs (NORA_PUBLIC_URL, S3 credentials, NPM_METADATA_TTL, Raw config)
- README restructured: tagline + docker run + GIF first, badges moved to Security section
- Remove hardcoded OpenSSF Scorecard version from README
- Verify blob digest (SHA256) on upload — reject mismatches with DIGEST_INVALID error
- Reject sha512 digests (only sha256 supported for blob uploads)
- Add upload session limits: max 100 concurrent, 2GB per session, 30min TTL (configurable via NORA_MAX_UPLOAD_SESSIONS, NORA_MAX_UPLOAD_SESSION_SIZE_MB)
- Bind upload sessions to repository name (prevent session fixation attacks)
- Add security headers: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Run containers as non-root user (USER nora) in all Dockerfiles
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Filter .meta.json from Docker tag list (fixes ArgoCD Image Updater tag recursion)
- Fix catalog endpoint to show namespaced images correctly (library/alpine instead of library)
- CodeQL workflow for SAST analysis
- SLSA provenance attestation for release artifacts
- Configurable upload session size for ML models via NORA_MAX_UPLOAD_SESSION_SIZE_MB (default 2048 MB)
- Docker dashboard: Namespaced images (library/alpine, grafana/grafana) now visible in UI — index builder finds manifests by position, not fixed index
- Docker proxy: Auto-prepend
library/for single-segment official Hub images (nginx, alpine, node) — no need to explicitly use library/ prefix - CI: Fixed cargo-deny license checks (NCSA for libfuzzer-sys, MIT for fuzz crate, unused-allowed-license config)
- Docker dashboard: Namespaced-образы (library/alpine, grafana/grafana) теперь отображаются в UI
- Docker proxy: Автоподстановка
library/для официальных образов Docker Hub (nginx, alpine, node) — больше не нужно указывать library/ вручную - CI: Исправлены проверки лицензий cargo-deny
- npm URL rewriting: Tarball URLs in proxied metadata now rewritten to point to NORA (previously tarballs bypassed NORA and downloaded directly from npmjs.org)
- npm scoped packages: Full support for
@scope/packagein proxy handler and repository index - npm publish:
PUT /npm/{package}accepts standard npm publish payload with base64-encoded tarballs - npm metadata TTL: Configurable cache TTL (
NORA_NPM_METADATA_TTL, default 300s) with stale-while-revalidate fallback - Immutable cache: SHA256 integrity verification on cached npm tarballs — detects tampering on cache hit
- npm URL rewriting: Tarball URL в проксированных метаданных теперь переписываются на NORA (ранее тарболы шли напрямую из npmjs.org)
- npm scoped packages: Полная поддержка
@scope/packageв прокси-хендлере и индексе репозитория - npm publish:
PUT /npm/{package}принимает стандартный npm publish payload с base64-тарболами - npm metadata TTL: Настраиваемый TTL кеша (
NORA_NPM_METADATA_TTL, default 300s) с stale-while-revalidate - Immutable cache: SHA256 проверка целостности npm-тарболов — обнаружение подмены при отдаче из кеша
- Path traversal protection: Attachment filename validation in npm publish (rejects
../,/,\) - Package name mismatch: npm publish rejects payloads where URL path doesn't match
namefield (anti-spoofing) - Version immutability: npm publish returns 409 Conflict on duplicate version
- Защита от path traversal: Валидация имён файлов в npm publish (отклоняет
../,/,\) - Проверка имени пакета: npm publish отклоняет payload если имя в URL не совпадает с полем
name(anti-spoofing) - Иммутабельность версий: npm publish возвращает 409 Conflict при попытке перезаписать версию
- npm proxy_auth:
proxy_authfield was configured but not wired intofetch_from_proxy— now sends Basic Auth header to upstream - npm proxy_auth: Поле
proxy_authбыло в конфиге, но не передавалось вfetch_from_proxy— теперь отправляет Basic Auth в upstream
- Dashboard: Docker upstream now shown in mount points table (was null)
- Dashboard: Docker namespaced repositories (library/alpine, grafana/grafana) now visible in UI
- Dashboard: npm proxy-cached packages now appear in package list
- Dashboard: Отображение Docker upstream в таблице точек монтирования (было null)
- Dashboard: Namespaced Docker-репозитории (library/alpine, grafana/grafana) теперь видны в UI
- Dashboard: npm-пакеты из прокси-кеша теперь отображаются в списке пакетов
- Upstream Authentication: All registry proxies now support Basic Auth credentials for private upstream registries
- Аутентификация upstream: Все прокси реестров теперь поддерживают Basic Auth для приватных upstream-реестров
- Docker:
NORA_DOCKER_UPSTREAMS="https://registry.corp.com|user:pass" - Maven:
NORA_MAVEN_PROXIES="https://nexus.corp.com/maven2|user:pass" - npm:
NORA_NPM_PROXY_AUTH="user:pass" - PyPI:
NORA_PYPI_PROXY_AUTH="user:pass"
- Docker:
- Plaintext credential warning: NORA logs a warning at startup if credentials are stored in config.toml instead of env vars
- Предупреждение о plaintext credentials: NORA логирует предупреждение при старте, если credentials хранятся в config.toml вместо переменных окружения
- Extracted
basic_auth_header()helper for consistent auth across all protocols - Вынесен хелпер
basic_auth_header()для единообразной авторизации всех протоколов
- Removed unused
DockerAuth::fetch_with_auth()method (dead code cleanup) - Удалён неиспользуемый метод
DockerAuth::fetch_with_auth()(очистка мёртвого кода)
- docker-compose.yml: Fixed image reference from
getnora/nora:latesttoghcr.io/getnora-io/nora:latest - docker-compose.yml: Исправлена ссылка на образ с
getnora/nora:latestнаghcr.io/getnora-io/nora:latest
- Authentication Guide: Added complete auth setup guide in README — htpasswd, API tokens, RBAC roles, curl examples
- Руководство по аутентификации: Добавлено полное руководство по настройке auth в README — htpasswd, API-токены, RBAC-роли, примеры curl
- FSTEC builds: Documented
Dockerfile.astraandDockerfile.redospurpose in README - Сборки ФСТЭК: Документировано назначение
Dockerfile.astraиDockerfile.redosв README - TLS / HTTPS: Added reverse proxy setup guide (Caddy, Nginx) and
insecure-registriesDocker config for internal deployments - TLS / HTTPS: Добавлено руководство по настройке reverse proxy (Caddy, Nginx) и конфигурация
insecure-registriesDocker для внутренних инсталляций
- Removed stale
CHANGELOG.md.bakfrom repository - Удалён устаревший
CHANGELOG.md.bakиз репозитория
- Configurable body limit:
NORA_BODY_LIMIT_MBenv var (default:2048= 2GB) — replaces hardcoded 100MB limit that caused413 Payload Too Largeon large Docker image push - Настраиваемый лимит тела запроса: переменная
NORA_BODY_LIMIT_MB(по умолчанию:2048= 2GB) — заменяет захардкоженный лимит 100MB, вызывавший413 Payload Too Largeпри push больших Docker-образов - Docker Delete API:
DELETE /v2/{name}/manifests/{reference}andDELETE /v2/{name}/blobs/{digest}per Docker Registry V2 spec (returns 202 Accepted) - Docker Delete API:
DELETE /v2/{name}/manifests/{reference}иDELETE /v2/{name}/blobs/{digest}по спецификации Docker Registry V2 (возвращает 202 Accepted) - Namespace-qualified DELETE variants (
/v2/{ns}/{name}/...) - Audit log integration for delete operations
- Docker push of images >100MB no longer fails with 413 error
- Push Docker-образов >100MB больше не падает с ошибкой 413
- Helm OCI support:
helm push/helm pullnow works out of the box via OCI protocol - Поддержка Helm OCI:
helm push/helm pullтеперь работают из коробки через OCI протокол - RBAC: Token-based role system with three roles —
read,write,admin(default:read) - RBAC: Ролевая система на основе токенов —
read,write,admin(по умолчанию:read) - Audit log: Persistent append-only JSONL audit trail for all registry operations (
{storage}/audit.jsonl) - Аудит: Персистентный append-only JSONL лог всех операций реестра (
{storage}/audit.jsonl) - GC command:
nora gc --dry-run— garbage collection for orphaned blobs (mark-and-sweep) - Команда GC:
nora gc --dry-run— сборка мусора для осиротевших блобов (mark-and-sweep)
- Helm OCI pull: Fixed OCI manifest media type detection — manifests with non-Docker
config.mediaTypenow correctly returnapplication/vnd.oci.image.manifest.v1+json - Helm OCI pull: Исправлено определение media type OCI манифестов — манифесты с не-Docker
config.mediaTypeтеперь корректно возвращаютapplication/vnd.oci.image.manifest.v1+json - Docker-Content-Digest: Added missing header in blob upload response (required by Helm OCI client)
- Docker-Content-Digest: Добавлен отсутствующий заголовок в ответе на загрузку blob (требуется клиентом Helm OCI)
- Read-only tokens (
role: read) are now blocked from PUT/POST/DELETE/PATCH operations with HTTP 403 - Токены только для чтения (
role: read) теперь блокируются при PUT/POST/DELETE/PATCH с HTTP 403
- Rate limiter fix: Added
NORA_RATE_LIMIT_ENABLEDenv var (default:true) to disable rate limiting on internal deployments - Исправление rate limiter: Добавлена переменная
NORA_RATE_LIMIT_ENABLED(по умолчанию:true) для отключения rate limiting на внутренних инсталляциях - SmartIpKeyExtractor: Upload and general routes now use
SmartIpKeyExtractor(readsX-Forwarded-For) instead ofPeerIpKeyExtractor— fixes 429 errors behind reverse proxy / Docker bridge - SmartIpKeyExtractor: Маршруты upload и general теперь используют
SmartIpKeyExtractor(читаетX-Forwarded-For) вместоPeerIpKeyExtractor— устраняет ошибки 429 за reverse proxy / Docker bridge
clap4.5.56 → 4.5.60uuid1.20.0 → 1.21.0tempfile3.24.0 → 3.26.0bcrypt0.17.1 → 0.18.0indicatif0.17.11 → 0.18.4
actions/checkout4 → 6actions/upload-artifact4 → 7softprops/action-gh-release1 → 2aquasecurity/trivy-action0.30.0 → 0.34.2docker/build-push-action5 → 6- Move scan/release to self-hosted runner with NORA cache
- Сканирование/релиз перенесены на self-hosted runner с кэшем через NORA
install.shinstaller script live at https://getnora.io/install.sh —curl -fsSL https://getnora.io/install.sh | sh- Скрипт установки
install.shдоступен на https://getnora.io/install.sh
- Restore Astra Linux SE Docker image build, Trivy scan, and release artifact (
-astratag) - Восстановлена сборка Docker-образа для Astra Linux SE, сканирование Trivy и артефакт релиза (тег
-astra)
- Binary (
nora) + SHA-256 checksum attached to every GitHub Release - Бинарник (
nora) и SHA-256 контрольная сумма прикреплены к каждому релизу GitHub
- Security: bump
prometheus0.13 → 0.14 (CVE-2025-53605) andbytes1.11.0 → 1.11.1 (CVE-2026-25541) - Безопасность: обновлены
prometheus0.13 → 0.14 (CVE-2025-53605) иbytes1.11.0 → 1.11.1 (CVE-2026-25541)
- Add Dependabot for automated dependency updates / Добавлен Dependabot для автоматического обновления зависимостей
- Pin
aquasecurity/trivy-actionto0.30.0, bump to0.34.1; scan gate blocks release on HIGH/CRITICAL CVE - Закреплён
trivy-action@0.30.0, обновлён до0.34.1; сканирование блокирует релиз при HIGH/CRITICAL CVE - Upgrade
codeql-actionv3 → v4 / Обновлёнcodeql-actionv3 → v4 - Fix
deny.tomldeprecated keys (copyleft,unlicensedremoved incargo-deny) / Исправлены устаревшие ключи вdeny.toml - Fix binary path in Docker image (
/usr/local/bin/nora) / Исправлен путь бинарника в Docker-образе - Pin build job to
norarunner label / Джоб сборки закреплён за runner'ом с меткойnora - Allow
CDLA-Permissive-2.0license (webpki-roots) / Разрешена лицензияCDLA-Permissive-2.0 - Ignore
RUSTSEC-2025-0119(unmaintained transitive depnumber_prefixviaindicatif)
chrono0.4.43 → 0.4.44quick-xml0.31.0 → 0.39.2toml0.8.23 → 1.0.3+spec-1.1.0flate21.1.8 → 1.1.9softprops/action-gh-release1 → 2actions/checkout4 → 6docker/build-push-action5 → 6
- Replace text title with SVG logo;
Ostyled in blue-600 / Заголовок заменён SVG-логотипом; букваOстилизована в blue-600
- First stable release with Docker images published to container registry
- Первый стабильный релиз с Docker-образами, опубликованными в container registry
- Consolidate all Docker builds into a single job to fix runner network issues / Все Docker-сборки объединены в один job для устранения сетевых проблем runner'а
- Build musl static binary for maximum portability / Сборка musl-бинарника для максимальной переносимости
- Add security scanning (Trivy) + SBOM generation to release pipeline / Добавлено сканирование безопасности (Trivy) и генерация SBOM в pipeline релиза
- Add Cargo cache to speed up builds / Добавлен кэш Cargo для ускорения сборок
- Replace
gitleaksGitHub Action with CLI (no license requirement) /gitleaksAction заменён CLI-вызовом (лицензия не требуется) - Use GitHub-runner's own Rust toolchain (avoid path conflicts) / Используется Rust toolchain самого GitHub-runner'а
- Use shared runner filesystem instead of artifact API (avoids network upload latency) / Общая файловая система runner'а вместо artifact API
- Remove Astra Linux build temporarily / Сборка для Astra Linux временно удалена
- Parallel CI builds for Astra Linux and RedOS / Параллельная сборка в CI для Astra Linux и RedOS
- Use
FROM scratchbase image for Astra Linux and RedOS Docker builds / Базовый образFROM scratchдля Docker-сборок Astra Linux и RedOS - Shared
reqwest::Clientacross all registry handlers / Общийreqwest::Clientдля всех registry-обработчиков
- Auth: replace
starts_withwith explicitmatches!for token path checks / Аутентификация:starts_withзаменён явной проверкойmatches!для путей с токенами - Remove unnecessary QEMU step for amd64-only builds / Удалён лишний шаг QEMU для amd64-сборок
- Pre-commit hook to prevent accidental commits of sensitive files / Pre-commit хук для защиты от случайного коммита чувствительных файлов
- README badges: build status, version, license / Бейджи в README: статус сборки, версия, лицензия
- In-memory repository index with pagination for faster dashboard load / Индекс репозитория в памяти с пагинацией для ускорения загрузки дашборда
- Use
div_ceilinstead of manual ceiling division / Использованdiv_ceilвместо ручной реализации деления с округлением вверх
- Logo styling refinements
- Copyright headers to all source files (Volkov Pavel | DevITWay)
- SPDX-License-Identifier: MIT in all .rs files
- N○RA branding: stylized O logo across dashboard
- Fixed O letter alignment in logo
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Code formatting (cargo fmt)
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Docker dashboard now shows actual image size from manifest layers (config + layers sum)
- Previously showed only manifest file size (~500 B instead of actual image size)
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - npm dashboard now shows correct version count and package sizes
- Parses metadata.json for versions, dist.unpackedSize, and time.modified
- Previously showed 0 versions / 0 B for all packages
- Rate limits now configurable via
config.tomland environment variables - New config section
[rate_limit]with parameters:auth_rps,auth_burst,upload_rps,upload_burst,general_rps,general_burst - Environment variables:
NORA_RATE_LIMIT_{AUTH|UPLOAD|GENERAL}_{RPS|BURST}
- Trait-based secrets management (
SecretsProvidertrait) - ENV provider as default (12-Factor App pattern)
- Protected secrets with
zeroize(memory zeroed on drop) - Redacted Debug impl prevents secret leakage in logs
- New config section
[secrets]withproviderandclear_envoptions
- Support for image metadata retrieval
- Bilingual onboarding guide (EN/RU)
- Internationalization (i18n) support
- PyPI registry proxy
- UI improvements
- Dark theme applied to all UI pages
- Version bump release
- Dashboard endpoint added to OpenAPI documentation
- Dynamic version display in UI sidebar
- Global stats panel: downloads, uploads, artifacts, cache hit rate, storage
- Extended registry cards with artifact count, size, counters
- Activity log (last 20 events)
- Dark theme (bg: #0f172a, cards: #1e293b)
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Docker push/pull: added PATCH endpoint for chunked uploads
- Go and Raw registries missing from Prometheus metrics (
detect_registrylabeled both as "other") (PR #97, @TickTockBent) - Go and Raw registries missing from
/healthendpointregistriesobject (PR #97, @TickTockBent) - Garbage collection scoped to Docker-only blobs — prevents GC from deleting non-Docker registry data (PR #109, @TickTockBent)
- Correct
zeroizeannotation placement and avoid secret cloning inprotected.rs(PR #108, @TickTockBent) - Rate limiting: health/metrics endpoints now exempt
- Increased upload rate limits for Docker parallel requests
- Replaced emoji icons with proper SVG brand icons (Simple Icons style)
- Docker, Maven, npm, Cargo, PyPI icons now render as scalable vector graphics
- Consistent icon styling across dashboard, sidebar, and detail pages
- Unit tests for LocalStorage (8 tests): put/get, list, stat, health_check
- Unit tests for S3Storage with wiremock HTTP mocking (11 tests)
- Integration tests for auth/htpasswd (7 tests)
- Token lifecycle tests (11 tests)
- Validation tests (21 tests)
- Total: 75 tests passing
- Path traversal protection: rejects
../,..\\, null bytes, absolute paths - Docker image name validation per OCI distribution spec
- Content digest validation (
sha256:[64 hex],sha512:[128 hex]) - Docker tag/reference validation
- Storage key length limits (max 1024 chars)
- Auth endpoints: 1 req/sec, burst 5 (brute-force protection)
- Upload endpoints: 10 req/sec, burst 20
- General endpoints: 100 req/sec, burst 200
- Uses
tower_governor0.8 withPeerIpKeyExtractor
X-Request-IDheader added to all responses- Accepts upstream request ID or generates UUID v4
- Tracing spans include request_id for log correlation
nora migrate --from local --to s3- migrate between storage backends--dry-runflag for preview without copying- Progress bar with indicatif
- Skips existing files in destination
- Summary statistics (migrated, skipped, failed, bytes)
AppErrorenum withIntoResponsefor Axum- Automatic conversion from
StorageErrorandValidationError - JSON error responses with request_id support
StorageErrornow usesthiserrorderive macroTokenErrornow usesthiserrorderive macro- Storage wrapper validates keys before delegating to backend
- Docker registry handlers validate name, digest, reference inputs
- Body size limit set to 100MB default via
DefaultBodyLimit
thiserror = "2"- typed error handlingtower_governor = "0.8"- rate limitinggovernor = "0.10"- rate limiting backendtempfile = "3"(dev) - temporary directories for testswiremock = "0.6"(dev) - HTTP mocking for S3 tests
src/validation.rs- input validation modulesrc/migrate.rs- storage migration modulesrc/error.rs- application error typessrc/request_id.rs- request ID middlewaresrc/rate_limit.rs- rate limiting configuration
- Multi-protocol support: Docker Registry v2, Maven, npm, Cargo, PyPI
- Web UI dashboard
- Swagger UI (
/api-docs) - Storage backends: Local filesystem, S3-compatible
- Smart proxy/cache for Maven and npm
- Health checks (
/health,/ready) - Basic authentication (htpasswd with bcrypt)
- API tokens (revocable, per-user)
- Prometheus metrics (
/metrics) - JSON structured logging
- Environment variable configuration
- Graceful shutdown (SIGTERM/SIGINT)
- Backup/restore commands