github getnora-io/nora v0.9.6

4 hours ago

[0.9.6] - 2026-06-27

Added

  • Anonymous Docker pull (auth.docker_anon_pull, NORA_AUTH_DOCKER_ANON_PULL) — a dedicated, default-off switch that serves docker pull without docker login. With auth enabled, an anonymous GET /v2/ returns a 401 Basic challenge (so docker login works); under anonymous_read = true the manifest/blob reads themselves were served anonymously, but the /v2/ ping still challenged. Whether a logged-out docker pull then 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 with no basic auth credentials (#778). When docker_anon_pull = true, the /v2/ ping returns 200 so anonymous pull works uniformly for both stores, and manifest/blob/tag reads are served without auth; writes (push/delete) still require a token, /v2/_catalog stays authenticated (no anonymous repository enumeration), and a request that carries an Authorization header is still validated (so docker login -u token -p <nra_…> and audit attribution keep working). The switch is independent of anonymous_read, so serving Maven/raw/npm anonymously never exposes container images. Behavior change: anonymous access to Docker /v2 read endpoints is now governed solely by docker_anon_pull. Deployments that pulled images anonymously under anonymous_read = true (containerd image store) must set docker_anon_pull = true to keep that working. Clients built on containers/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 /health response gains an upstreams section, one entry per enabled proxy registry, so operators without Prometheus/Grafana can see which upstreams are reachable (previously this was only on the nora_circuit_breaker_state gauge). Each entry reports statusclosed / open / half_open (mirroring the gauge labels), or disabled when the circuit-breaker feature is off (the default) — plus failure_count and last_failure_seconds_ago. The state is read from the breaker's cached in-memory snapshot, so /health never performs a live upstream probe and stays fast and non-blocking; an enabled registry with no recorded breaker yet defaults to a healthy closed. The OpenAPI HealthResponse schema is updated to match (#773).

Fixed

  • A present-but-empty [<registry>] table now keeps the default upstream — npm/pypi proxy and maven/docker proxies/upstreams used a bare #[serde(default)] that deserialized to None/[], diverging from the Default impl's real upstream. Writing [npm] (or [pypi]/[maven]/[docker]) in config.toml to 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 the Default impl, 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 with min_release_age and no quarantine was not flagged, and a Docker-only [curation.docker] quarantine was 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 publish against the Cargo registry no longer 404s — the sparse-index config.json advertised its api base as {base}/cargo/api. Cargo appends /api/v1/... to that base, so publish requests went to /cargo/api/api/v1/crates/new and returned 404. The advertised api base is now the registry mount ({base}/cargo), so Cargo builds /cargo/api/v1/crates/new and resolves to the mounted route; tests cover both the metadata and publish routes derived from the config.json api base (#783).

Security

  • A per-registry-only quarantine now loads its durable store — the digest-quarantine store was loaded only when the global curation.quarantine was set. A Docker-only [curation.docker] quarantine got 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 global curation.quarantine = "off" no longer loads the store (it has no effect to enforce); set a real mode (observe/enforce) where you want enforcement.

Performance

  • Index rebuild drops the per-key stat() — rebuild walked storage.list() and then issued a separate storage.stat() per key for size/mtime; on S3 that stat() 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 additive StorageBackend::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 to list() + per-key stat(), so every other backend stays correct, and gc/retention/backup/mirror keep using list() unchanged; a counting-backend test asserts the rebuild makes zero per-key stat() calls (#759).

Install

# x86_64
curl -LO https://github.com/getnora-io/nora/releases/download/v0.9.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/v0.9.6/nora-linux-arm64
chmod +x nora-linux-arm64
sudo mv nora-linux-arm64 /usr/local/bin/nora

Docker

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

DEB / RPM

# Debian / Ubuntu / Astra Linux (amd64)
curl -LO https://github.com/getnora-io/nora/releases/download/v0.9.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/v0.9.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.