Added
- 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).
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/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).
Fixed
- Per-registry artifact count (UI stats) —
/api/ui/statsandnora_artifacts_totalno longer over-count Maven: a metadata-only directory (the artifact-levelmaven-metadata.xml) is no longer counted as a repository, so the count matches/api/ui/dashboardand 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
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).
Security
- 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
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/noraDocker
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.rpmChangelog
See CHANGELOG.md