github getnora-io/nora v0.9.4

12 hours ago

Added

  • Multiple PyPI upstream proxiesNORA_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-url ahead 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_V6ONLY cleared via socket2), with a 0.0.0.0 fallback when IPv6 is unavailable, so the default container bind serves both (#696).
  • Docker OCI single-POST monolithic blob uploadPOST /v2/<name>/blobs/uploads/?digest=... is now supported per the OCI Distribution spec (#698).
  • Docker Range requests for blob GETRange / 206 Partial Content enables resumable image pulls (#657).
  • nora healthcheck CLI subcommand — a dependency-free loopback probe for a Docker HEALTHCHECK; it ignores HTTP_PROXY and probes IPv4 loopback so it reaches a wildcard or 0.0.0.0 bind (#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-Match on 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_url is unset on a loopback bind (#591).
  • Docker default_action = deny — reject image names that match no configured upstream rule (#572).

Changed

  • 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 /metrics can 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-stale behavior is aligned across all registry handlers (#576, #577).
  • Client-facing URL construction (service-index rewriting, UI install commands, docker pull) is centralized in ServerConfig::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).

Fixed

  • Per-registry artifact count (UI stats)/api/ui/stats and nora_artifacts_total no longer over-count Maven: a metadata-only directory (the artifact-level maven-metadata.xml) is no longer counted as a repository, so the count matches /api/ui/dashboard and on-disk reality (#714).
  • 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 fetch calls, redirect Location headers, and the API-docs / Swagger URLs are now prefixed with the path component of public_url; root-vhost deploys are unaffected (the prefix is empty, a no-op) (#685, #686, #690). The UI docker pull command uses the bare host authority, and the IPv6 fallback base URL brackets the address (http://[::1]:4000).
  • PyPI — percent-encoded filenames (e.g. +cuXXX wheels 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 HEALTHCHECK uses 127.0.0.1 and 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 integrityget() fails closed on a hash-pin mismatch (#582, #600); hash-pin writes are durable and recorded before put() returns (#604, #613, #633); the streaming Docker-blob serve verifies the digest while streaming and aborts on tamper (#632); health_check write-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 as nora_leak_detection_skipped_total{reason="own_surface"}, so nora_response_upstream_url_leak_total reflects only genuine proxy-response leaks and is alertable (#624).
  • Secrets — the env provider preserves VarError context in errors (#592).

Security

  • Min-release-age quarantine now fails closed on an unknown publish dateMinReleaseAgeFilter returned Skip (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 rejects on_failure = "open"). An unknown date is now blocked when the quarantine is active for that registry (threshold > 0); a registry with the quarantine disabled (threshold 0) 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_scope is 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's namespace_scope now restricts which artifact namespaces its tokens may publish to, across docker, raw, npm, maven, pypi and cargo. Matching is segment-aware (myorg/* matches myorg/repo but never myorg-evil/...; use myorg/** for everything under myorg/).
    • BREAKING (behavioral): if a provider's namespace_scope is set to anything other than ["*"], out-of-scope writes from that issuer now return 403. 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 as would_deny via the new nora_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.

Install

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

Docker

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

DEB / RPM

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