github getnora-io/nora v1.3.2

6 hours ago

[1.3.2] - 2026-09-19

Added

  • CPAN proxy registry (#1004) — NORA can now proxy and cache CPAN indexes and distribution archives, apply curation controls, and browse cached distributions with all available versions in the web UI.

Changed

  • lock-audit runs in CI (#980) — the check guards a documented invariant (publish_lock serializes all writes to the same artifact path) but was referenced only from the Makefile, so nothing ran it and a red make check reached v1.3.0. It is now a step in the Coherence job, with its self-test running first: a heuristic over source text that breaks prints nothing, which is indistinguishable from a clean tree. The self-test also runs the audit under every awk on the image, so a gawk-only construct creeping back in fails the job instead of quietly disabling the check.
  • No spurious htpasswd warning in OIDC-only deployments (#996)Auth enabled but htpasswd file not found or empty was logged at warn even when OIDC providers are configured and are the intended (and only) write path, training operators to ignore auth warnings. With OIDC active it is now an info line (Basic auth disabled (no htpasswd); OIDC is the write path); the warn remains when neither htpasswd nor OIDC is configured.

Fixed

  • Audit log records the real actor, not api (#985) — every audit entry used the literal "api" as the actor, so a delete or overwrite made with an admin token and one made by a Basic-auth user (or anonymously) were indistinguishable in the security log. Write events (push, delete, overwrite, reindex) now record the authenticated username, or anonymous when the request carries no identity; background cache events (proxy fetch, cache hit, local pull) record proxy.
  • Docker repository page cost is bounded, not registry-wide (#986) — opening /ui/docker/{name} listed every key under docker/ to find namespaced copies of the image, so each page view grew slower as the whole registry grew (on an object store, a listing whose size tracks the total object count). It now lists only the image's own prefixes — docker/{name}/manifests/ and, for each configured upstream, docker/{namespace}/{name}/manifests/ — a bounded number of requests independent of how many unrelated repositories exist. Namespaced tags are still shown.
  • OpenAPI /health documents registries as an open map (#987) — the schema described registries as a fixed struct with seven named fields (docker, maven, npm, cargo, pypi, go, raw), so clients generated from the document missed every format added since (RubyGems, Terraform, Ansible, NuGet, Pub, Conan, RPM, Debian, CPAN) and treated disabled formats as always present. It now matches the runtime HashMap<String, String> of enabled registries — an additionalProperties string map keyed by any RegistryType::all() name.
  • PyPI simple index reads file hashes from one listing (#991)GET /simple/{name}/ read each locally stored file's .sha256 sidecar with its own storage request, so a package with N files cost N+1 round-trips per response in both the HTML and the PEP 691 JSON form; on an object store each is a network request. Every stored file now also gets an empty marker object whose key carries its digest (pypi-sha256/<project>/<filename>/<sha256>), and the index reads every digest from one listing, so its cost no longer depends on the number of files. Files stored before the upgrade are served from their sidecars and get their markers in the background; a file with no marker, or with more than one, is always served from its sidecar, which is still written and stays authoritative. Retention removes a file's markers together with the file.
  • Image tags move only after the builds, packages and provenance have succeeded (#992) — the release workflow pushed the RED OS and Astra images under their version and latest-* tags in its first job, before the arm64 image, the deb/rpm packages and SLSA provenance were built, and the alpine tags moved before the packages and provenance existed; when a later job failed, public channels pointed at a version with no GitHub Release. Every image is now pushed by digest only, and a single job moves all public tags — alpine, RED OS and Astra, in GHCR and Docker Hub — once the arm64 image, packages and provenance have succeeded. Before any tag moves it starts each variant from its digest and checks /health; after promotion it checks that every promoted tag resolves to the expected digest in both registries and starts the promoted multi-arch alpine tag. Re-running the job promotes the same digests. The GitHub Release is still created after promotion, so if that last job fails the tags stay public until it is re-run. RED OS and Astra images are now signed with cosign, and the post-release gate verifies their signatures.
  • OIDC token rejections are logged and counted (#994) — a rejected OIDC token returned a generic 401 while the reason (a lifetime ceiling, a wrong aud, a bad signature, a missing role rule) was discarded, so operators could not tell the cases apart. The auth middleware now emits a warn event carrying the full reason (never the token) and increments nora_auth_oidc_rejected_total{reason} with a bounded reason set (disabled, no_provider, lifetime_exceeded, no_role_rule, algorithm, jwt_invalid, jwks, other).
  • Serve Android APKs with their package MIME type (#997) — raw .apk downloads now use application/vnd.android.package-archive, allowing Android browsers to hand them directly to the package installer.
  • A proxied Docker blob streams while it is still being fetched (#1001) — a cache miss used to download the whole blob to a temp file, store it, and only then start the response, so a client saw nothing, not even the status line, until the blob was fully cached. containerd 2.3 caps that wait at a hard-coded 30 s (ResponseHeaderTimeout, not configurable from hosts.toml), retries once and gives up, and each abandoned request deleted the partial spool; on a slow upstream link every layer over a few hundred MB was re-fetched from scratch on every attempt and never cached — one 1.27 GB layer was started 56 times in four days without completing once. The response now goes out as soon as upstream has answered, the body follows the spool file as it grows, and the fill runs detached from the request, so a client that gives up mid-fill still leaves a cached blob behind for its retry. The digest is still verified before the blob is stored, and the last byte of the response is withheld until that verdict is in, so a poisoned upstream can never complete a Content-Length body; a mismatch, which was a 502 sent before any bytes, is now an aborted body on a 200, the same failure mode the cached serve path already has. A proxied digest enters quarantine only once its fetch has verified, so a failed or poisoned fetch no longer starts the cooldown clock for the real blob.

Install

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

Docker

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

DEB / RPM

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