[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-auditruns in CI (#980) — the check guards a documented invariant (publish_lockserializes all writes to the same artifact path) but was referenced only from theMakefile, so nothing ran it and a redmake checkreached 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 emptywas logged atwarneven 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 aninfoline (Basic auth disabled (no htpasswd); OIDC is the write path); thewarnremains 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, oranonymouswhen the request carries no identity; background cache events (proxy fetch, cache hit, local pull) recordproxy. - Docker repository page cost is bounded, not registry-wide (#986) — opening
/ui/docker/{name}listed every key underdocker/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
/healthdocumentsregistriesas an open map (#987) — the schema describedregistriesas 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 runtimeHashMap<String, String>of enabled registries — anadditionalPropertiesstring map keyed by anyRegistryType::all()name. - PyPI simple index reads file hashes from one listing (#991) —
GET /simple/{name}/read each locally stored file's.sha256sidecar 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
401while the reason (a lifetime ceiling, a wrongaud, a bad signature, a missing role rule) was discarded, so operators could not tell the cases apart. The auth middleware now emits awarnevent carrying the full reason (never the token) and incrementsnora_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
.apkdownloads now useapplication/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 fromhosts.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 aContent-Lengthbody; a mismatch, which was a502sent before any bytes, is now an aborted body on a200, 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/noraDocker
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.rpmChangelog
See CHANGELOG.md