github getnora-io/nora v1.3.6

3 hours ago

[1.3.6] - 2026-10-15

Changed

  • mimalloc is the default global allocator (#1101) — the nora binary now allocates through mimalloc (feature mimalloc, on by default; build --no-default-features for the system allocator). On the static musl build, musl's allocator takes one global lock on every allocation, which under many async workers degrades into kernel futex contention and a metastable collapse — the process burns every core while throughput falls below its own maximum. Measured on the release build: an allocation-heavy request (tags/list, ~63k allocations per request) at 200 rps drove the musl build to 136 rps with p99 14.4 s on all 16 cores, and a 32-worker profile collapsed at ~100 rps; with mimalloc the same load served 200 rps at p99 ~24 ms on ~4 cores and never collapsed, and light requests gained ×2.4–2.6 in peak throughput. Built with mimalloc's no_thp feature so idle RSS stays ~16 MB. glibc builds benefit less, since glibc's malloc already uses per-thread arenas. Fixed by @clain23.

Fixed

  • An API token works as the Basic password without htpasswd (#1094) — with auth enabled and no htpasswd file, the OIDC-only setup from #996, every Basic request was refused with 401 Basic auth not configured before API tokens were checked, so docker login, twine and Maven could not use an nra_ token that Bearer accepted. The Basic path now checks htpasswd when there is one and then the token store, with the same role gates (a read token gets 403 on writes, /api/v1/admin/* needs an admin token), 503 on a token-store failure and 401 counted toward the lockout for a wrong token. With htpasswd configured nothing changes. The OIDC-only startup line now reads No htpasswd (OIDC-only): Basic auth accepts API tokens only instead of Basic auth disabled (no htpasswd); OIDC is the write path.
  • A curation refusal's hint runs as it is, and unparsable durations stop startup (#1099) — the 403 hint named nora curation explain <package>@<version>, which fails without the registry prefix the command expects; it now names registry:package@version (without @version when the request named none), quoted for a shell when the name needs it, and curation explain reads a scoped npm name such as npm:@scope/pkg@1.0.0. The docs link pointed at docs.getnora.dev, which has no DNS record; it is now https://getnora.dev/configuration/curation/. A quarantine_ttl that does not parse, such as "30m", became 14 days without a word, and an unparsable min_release_age stopped startup only in enforce mode; every min_release_age and quarantine_ttl, global and per registry, is now checked at startup in every mode, and one that does not parse is a configuration error that names it. A duration too large to count is refused instead of wrapping into a negative or small value.
  • uv installs from a PyPI index whose files have no hash (#1100) — NORA left the hashes key out of a file in the PEP 691 JSON index when it had no digest for it: a proxied upstream link without #sha256= (a mirror that publishes none) or a stored file without its checksum sidecar. PEP 691 requires the key on every file, and uv refused the whole index with missing field hashes; pip reads the HTML index and was not affected. Such a file is now listed with "hashes": {}.
  • OIDC: one JWKS refresh at a time, with a backoff after a failure (#1102) — after a restart every request carrying an OIDC token ran discovery and a JWKS fetch on its own, and a failed fetch was not remembered, so under load the herd repeated until one fetch got through; when discovery timed out the fallback {issuer}/.well-known/jwks.json was fetched, which GitHub's issuer answers 404 (its JWKS is at /.well-known/jwks), and about 1000 requests were rejected in 5 minutes on one instance. Now one request refreshes the JWKS while the others wait for its result, a failed fetch with nothing cached makes the next 5 s of requests fail fast (stale keys are still served when present), and the JWKS URI from a successful discovery is reused on later refreshes.
  • ruzstd 0.8.3 → 0.9.1 (RUSTSEC-2026-0344) — a crafted .zst input under 128 KB could make ruzstd allocate several gigabytes at once (a compressed block's decompressed size was not checked against Block_Maximum_Size), crashing the process with an out-of-memory condition — a denial of service. ruzstd decodes zstd-compressed rpm and deb payloads. 0.9.1 enforces the per-block output bound. Fixed by @clain23.
  • Docker pull counts no longer trip a false integrity alarm (#1114) — a Docker manifest pull rewrote the manifest's .meta.json on every request to bump its download count, racing the integrity-pin read a concurrent pull performs; a reader that caught the file mid-rewrite saw bytes that no longer matched the pin, and the integrity gate logged INTEGRITY VIOLATION on healthy data nobody had tampered with — about 21 per 100 concurrent pulls of one manifest on the local backend, clients still receiving 200. Pulls now record counts in memory and flush them in batches to a separate .stats.json sidecar, so .meta.json is immutable after publish and the request path no longer writes it. S3/GCS carry the pin in object metadata and were not affected.
  • rustls 0.23.45 → 0.23.46 (RUSTSEC-2026-0346/-0347/-0348/-0349) — four low/medium advisories in the TLS stack (client session Debug impls exposing secrets, buffered data after ECH rejection, unauthenticated ServerHello downgrade, unauthenticated Brotli certificate decompression up to 1 GiB). All fixed in rustls 0.23.46; a lock-only bump, no code change.

Security

  • UI token creation binds the new token to the caller's own identity and authority (GHSA-3j8r-8wjq-9xqw) — the UI create-token handler (POST /api/ui/tokens/create) took the new token's owner from a client-suppliable Basic username and its role from the submitted form without a role check, so an authenticated user could mint a token owned by another principal or carrying the admin role — the create-path sibling of the list and revoke issues already fixed in the #736 / GHSA-78cx-cfhm-rgmx family, missed at the time. The created token is now owned by the authenticated caller, minting the admin role requires the caller to already hold admin authority, and a namespace-scoped caller is refused before anything is minted.
  • A namespace-scoped admin identity can no longer mint an unrestricted token (GHSA-3j8r-8wjq-9xqw) — the admin control-plane mint (POST /api/v1/admin/tokens) minted an always-unrestricted token without consulting the caller's namespace authority, so an identity holding the admin role but a restricted namespace scope (for example an OIDC role_rule that grants admin together with a namespace_scope) could mint a token that writes outside its own scope, escalating past the boundary enforced on its own direct writes. A namespace-scoped caller is now refused fail-closed before the token is minted; an unrestricted admin is unaffected.
  • Raw downloads are served inert, and the edge rate limiters key untrusted peers by address (GHSA-3j8r-8wjq-9xqw) — a file uploaded to the raw registry was served under its own content-type, so an uploaded HTML or SVG rendered as a page at NORA's own origin with any inline script running in the viewer's session; raw GET and ranged GET now carry Content-Disposition: attachment and a per-response Content-Security-Policy: sandbox, so the browser saves rather than renders, and the global CSP layer became if_not_present so the stricter per-response policy survives while the UI's own policy is unchanged. Separately, the upload and general rate limiters resolved the client key from X-Forwarded-For supplied by any peer, so a client at an untrusted peer could mint a fresh limiter bucket per request by varying the header; untrusted peers are now keyed by their TCP address and X-Forwarded-For is honoured only from configured trusted proxies, the model the auth limiter already used (#541).

Install

# x86_64
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.6/nora-linux-amd64
chmod +x nora-linux-amd64
sudo mv nora-linux-amd64 /usr/local/bin/nora

# ARM64 (Apple Silicon, Graviton, Ampere)
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.6/nora-linux-arm64
chmod +x nora-linux-arm64
sudo mv nora-linux-arm64 /usr/local/bin/nora

Docker

docker pull getnora/nora:1.3.6
Variant Image Platforms
Alpine (default) getnora/nora:1.3.6 amd64, arm64
RED OS getnora/nora:1.3.6-redos amd64
Astra Linux SE getnora/nora:1.3.6-astra amd64
GHCR ghcr.io/getnora-io/nora:1.3.6 amd64, arm64

DEB / RPM

# Debian / Ubuntu / Astra Linux (amd64)
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.6/nora-amd64.deb
sudo dpkg -i nora-amd64.deb

# RHEL / Fedora / RED OS (amd64)
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.6/nora-amd64.rpm
sudo rpm -i nora-amd64.rpm

Changelog

See CHANGELOG.md

Don't miss a new nora release

NewReleases is sending notifications on new releases.