github getnora-io/nora v1.4.4

3 hours ago

[1.4.4] - 2026-11-07

Added

  • Docker upstreams can own namespaces (#1188) — namespaces = ["team-*"] on a [[docker.upstreams]] entry binds names by their first path segment (an exact segment, or a stem followed by *) to that upstream. A bound name is asked of that upstream only, with the whole path (no prefix stripping, no library/ retry), its copy is stored under that upstream's namespace, and whatever the upstream answers is final: before, under default_action = "allow", a 401 or a missing tag at the private registry let the next upstream serve a same-named public image. An upstream with namespaces is no longer asked for any other name, under either default_action. NORA_DOCKER_PROXIES takes them as a fourth field, url|auth|prefix|team-*;ops. Overlapping namespaces, a namespace that holds for a prefix, and an entry that is not a segment or a stem with * refuse to start. Downgrading to 1.4.3 or older ignores namespaces and puts the private registry back into the fallback chain; switch those names to an explicit host (prefix and default_action = "deny") before downgrading.

Changed

  • A [[docker.upstreams]] entry with an unknown key, or a prefix with * or ?, refuses to start (#1188) (Action may be needed) — an unknown key in an upstream entry used to be listed in the startup warning and ignored, so a misspelt namespaces would have left a private registry in the fallback chain. A prefix is matched literally, so prefix = "*" matched no name; under default_action = "deny" it refused every image that no other prefix routed. Both are now startup errors that name the entry. Copies of public names are stored under the first upstream without namespaces, which is the first upstream for every configuration that does not use them. The 403 DENIED refusal under default_action = "deny" now says that no prefix, namespaces or registry host routes the name; it named only prefix.
  • A Docker image pushed under a registry host or an upstream prefix is kept under that name (Action may be needed) — a push to nora.example/quay.io/org/img was stored as org/img, and one to nora.example/hub/acme/tool (hub an upstream prefix) as acme/tool (see Security). Such a push is now its own repository, kept apart from bare names and from the copies an upstream serves under its namespace; docker.io, index.docker.io and registry-1.docker.io still name the bare repository. The catalog, the repository list and the web UI show it under its full name. Images pushed this way by 1.4.3 or older stay where they were stored, under the shorter name, and are still served there; push them again to have them under the full name. A rollback to 1.4.3 does not see what 1.4.4 pushed under a host or prefix. An upstream namespace or prefix of _ stops the start.

Fixed

  • npm search works through NORA (#1148) — NORA forwarded the path of an npm registry endpoint such as /-/v1/search but not its query string, so the upstream got a search with no text (registry.npmjs.org: 400 'text' query parameter is required) and npm search failed with E404 'search' is not in this registry. The query now reaches the upstream as the client sent it. Under a namespace filter (curation.internal_namespaces) or with scopes owned by a private upstream (npm.proxies scopes), a search that names an internal package or such a scope — @internal/x, scope:vendor, corp-billing matching corp-* — is refused with 422 and the reason, and is not sent upstream, as an npm audit carrying such names already is; npm prints the reason.
  • A proxied Docker miss carries an OCI error the client prints (#1189) — when no upstream had a manifest or blob, NORA answered a bare 404 (or 502/503) with an empty body, and docker pull printed only Error response from daemon: unknown:, so a missing tag could not be told from a broken proxy. Every miss on the proxy path now answers application/json with an OCI error: 404 MANIFEST_UNKNOWN (with the name and reference) or 404 BLOB_UNKNOWN (with the digest), as a miss of a pushed image already did; 502 UNAVAILABLE when every upstream failed; 503 UNAVAILABLE with Retry-After when an upstream was skipped for its open circuit breaker; and, as before, 502 DENIED when an upstream refused NORA's credentials. docker pull now prints manifest unknown; dockerd does not print the body of a 5xx.
  • systemctl reload nora works with the shipped units (#1190) — the deployment guide says systemctl reload nora re-reads the configuration, but the unit in the .deb/.rpm packages, deploy/nora.service and the one install.sh writes had no ExecReload, so systemd refused the job (Job type reload is not applicable for unit nora.service) and the only way to apply a new curation policy without a restart was kill -HUP by hand. The three units now send the same SIGHUP (ExecReload=/bin/kill -HUP $MAINPID); the process keeps running and its PID is unchanged. After upgrading the package, systemctl daemon-reload picks up the new unit (the package scripts do it); a unit installed by install.sh is updated by running it again.
  • A Docker image name behind a registry host no upstream serves is no longer answered by another registry (#1195) — nora.example/quay.io/project1/service1 with only Docker Hub configured was stripped to project1/service1 and asked of the fallback chain, so whoever registered that name on Docker Hub decided what a client asking for quay.io received, and a cached Docker Hub copy was served under the quay.io name. Such a name is now asked of no upstream: it is answered from NORA's storage only (an image pushed to NORA under that host is still served) and is otherwise 404; under default_action = "deny" it is refused as before. A host names an upstream by its storage namespace or by the host of its url, so registry.gitlab.com/… reaches an upstream at https://registry.gitlab.com, whose namespace is gitlab.com. docker.io, index.docker.io and registry-1.docker.io all name Docker Hub; without a Docker Hub upstream they go to the fallback chain as before. To proxy another registry by its host, add it as an upstream.
  • A Docker upstream that refuses NORA's credentials is a 502 that says so (#1196) — an upstream that answered 401 with a challenge NORA cannot answer with a token (Harbor's Basic realm="harbor" for a robot account without access to the project), or whose token server refused the credentials with 401 or 403 (Docker Hub's "incorrect username or password"), was logged as "every upstream failed" and answered with an empty 502, as an outage would be. Both are now logged at warn with the upstream's URL, as "upstream refused NORA's credentials" or, with no credentials configured for it, "upstream requires credentials and none are configured for it", and answered with 502 and an OCI DENIED body that names the refused credentials. As before, such a refusal does not count toward opening the circuit breaker, and in the fallback chain another upstream's answer still decides. A 401 that persists after the token server issued a token stays a 404: Docker Hub answers a repository that does not exist that way, so it cannot be told from a refusal.
  • The Traefik example in deploy/traefik.yml no longer cuts a docker push at 60 s (#1197) — its websecure entry point kept Traefik v3's default readTimeout, so a layer whose upload took longer than a minute was ended with 499 and the push failed. The example now sets transport.respondingTimeouts.readTimeout: 0; a deployment copied from it should add the same, or a value longer than its slowest push.
  • An open circuit breaker on one Maven or Docker upstream no longer stops the request (#1198) — with circuit_breaker.enabled = true, an upstream whose breaker was open ended a Maven download, a Docker manifest or a Docker blob with 503, although the next configured upstream would have served it; Maven also kept one breaker for all of maven.proxies, so a dead first upstream opened it for the others too. Each Maven upstream now has a breaker of its own, keyed maven:<upstream URL> like Docker, PyPI and npm (the nora_circuit_breaker_state and nora_circuit_breaker_rejections_total series move from registry="maven" to registry="maven:<url>"; match registry=~"maven(:.*)?"; [circuit_breaker.overrides.maven] still applies to every Maven upstream). An upstream whose breaker is open is skipped and the next one asked. When none serves, an upstream's 404 decides as before; otherwise the answer is the 503 with Retry-After it was, and a cached copy that could not be revalidated is served stale instead, as when the upstream fails. A Docker name bound to one upstream through namespaces has no next upstream and still gets 503.
  • The Terraform module download goes through the circuit breaker (#1202) — GET /terraform/v1/modules/{ns}/{name}/{provider}/{version}/download asked its upstream with a bare HTTP client, past the terraform circuit breaker that every other Terraform path reports to: an open breaker did not stop module downloads (each one waited on a dead upstream and answered 502), and a failing download never counted toward opening it. The download now reports to the breaker: an open breaker answers 503 with Retry-After without asking the upstream, a 5xx, network error or timeout counts as a failure, and, as on the other Terraform paths, a 5xx or a network error is retried once before NORA answers. A 4xx is still 404, and a reply without X-Terraform-Get is still 502. terraform init honours the Retry-After of a 503, so with the breaker open it waits about 30 seconds before it fails, as it already did on the other Terraform paths.
  • Registry reads and writes are rate limited separately, as documented (#1205) — every registry request, a cargo download or a sparse-index read as much as a push, spent the upload_* bucket, while general_* only added a second limit to /api/tokens, so raising NORA_RATE_LIMIT_GENERAL_* did nothing for the 429s of a cold cargo install or dotnet restore. Now writes (PUT, POST, PATCH, DELETE) spend upload_* and reads (GET, HEAD, OPTIONS, and npm's audit POST) spend general_*, each its own bucket per client, so reading hard no longer starves a client's own publishes, nor the other way round; auth routes answer to auth_* alone. The general_* defaults rise from 100/s, burst 200 to 1000/s, burst 2000. Until 1.5 a read gets the larger of general_* and upload_*, setting by setting, so a configuration that kept general_* below upload_* — the helm chart's 100/200, the documented presets, an upload_* raised to cure read 429s — reads no harder than before, and NORA warns at startup; from 1.5 reads are limited by general_* alone. Rate limits enforced in the startup log now reports write_* and read_* in place of upload_* and general_*.
  • A Terraform registry module installs through NORA (#1209) — NORA answered every module download with X-Terraform-Get rewritten to its own …/source endpoint, and neither kind of source survived that with the Terraform CLI: a git:: source (most public modules on registry.terraform.io) was fetched from its repository address as a plain web page, which NORA served as application/zip and cached for a year (error downloading …: no source URL was returned), and an archive source lost what tells go-getter it is an archive (XML syntax error … U+001F). Now an HTTP source that names an archive — an extension such as .tar.gz or .zip, an archive= parameter, or a GitHub codeload URL — is answered as …/source?archive=<kind> with its //subdir kept, and downloaded and cached through NORA as before; what the source answers is served and cached only when it is an archive (otherwise 502). A git::/hg:: source, or an HTTP address that names no archive, is answered unchanged and Terraform fetches it itself, so the client needs access to the git host; serving git sources from NORA for air-gapped networks is #1213. A source address stored by an earlier release for a git module is no longer used: the upstream is asked again.

Security

  • A Docker push under a registry host or an upstream prefix wrote another repository (GHSA-wh2m-j55g-27ch) — the host or prefix was dropped from the name a hosted push was stored under, while the write permission (OIDC namespace_scope) was checked on the name as pushed. A push to quay.io/org/img replaced the tags of org/img and answered for it, and an identity whose scope allowed a host-like first segment could overwrite a repository outside its scope. A push is now stored under the name it was pushed as; see Changed.

Install

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

Docker

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

DEB / RPM

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