[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 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).
Fixed
- 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).
Security
- 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.
Performance
- 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).
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/noraDocker
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.rpmChangelog
See CHANGELOG.md