[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, nolibrary/retry), its copy is stored under that upstream's namespace, and whatever the upstream answers is final: before, underdefault_action = "allow", a 401 or a missing tag at the private registry let the next upstream serve a same-named public image. An upstream withnamespacesis no longer asked for any other name, under eitherdefault_action.NORA_DOCKER_PROXIEStakes them as a fourth field,url|auth|prefix|team-*;ops. Overlapping namespaces, a namespace that holds for aprefix, and an entry that is not a segment or a stem with*refuse to start. Downgrading to 1.4.3 or older ignoresnamespacesand puts the private registry back into the fallback chain; switch those names to an explicit host (prefixanddefault_action = "deny") before downgrading.
Changed
- A
[[docker.upstreams]]entry with an unknown key, or aprefixwith*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 misspeltnamespaceswould have left a private registry in the fallback chain. Aprefixis matched literally, soprefix = "*"matched no name; underdefault_action = "deny"it refused every image that no otherprefixrouted. Both are now startup errors that name the entry. Copies of public names are stored under the first upstream withoutnamespaces, which is the first upstream for every configuration that does not use them. The403 DENIEDrefusal underdefault_action = "deny"now says that noprefix,namespacesor registry host routes the name; it named onlyprefix. - 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/imgwas stored asorg/img, and one tonora.example/hub/acme/tool(huban upstreamprefix) asacme/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.ioandregistry-1.docker.iostill 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 upstreamnamespaceorprefixof_stops the start.
Fixed
npm searchworks through NORA (#1148) — NORA forwarded the path of an npm registry endpoint such as/-/v1/searchbut not its query string, so the upstream got a search with notext(registry.npmjs.org:400 'text' query parameter is required) andnpm searchfailed withE404 '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.proxiesscopes), a search that names an internal package or such a scope —@internal/x,scope:vendor,corp-billingmatchingcorp-*— is refused with422and the reason, and is not sent upstream, as annpm auditcarrying 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(or502/503) with an empty body, anddocker pullprinted onlyError response from daemon: unknown:, so a missing tag could not be told from a broken proxy. Every miss on the proxy path now answersapplication/jsonwith an OCI error:404 MANIFEST_UNKNOWN(with the name and reference) or404 BLOB_UNKNOWN(with the digest), as a miss of a pushed image already did;502 UNAVAILABLEwhen every upstream failed;503 UNAVAILABLEwithRetry-Afterwhen an upstream was skipped for its open circuit breaker; and, as before,502 DENIEDwhen an upstream refused NORA's credentials.docker pullnow printsmanifest unknown; dockerd does not print the body of a 5xx. systemctl reload noraworks with the shipped units (#1190) — the deployment guide sayssystemctl reload norare-reads the configuration, but the unit in the.deb/.rpmpackages,deploy/nora.serviceand the oneinstall.shwrites had noExecReload, 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 waskill -HUPby 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-reloadpicks up the new unit (the package scripts do it); a unit installed byinstall.shis 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/service1with only Docker Hub configured was stripped toproject1/service1and 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 otherwise404; underdefault_action = "deny"it is refused as before. A host names an upstream by its storage namespace or by the host of itsurl, soregistry.gitlab.com/…reaches an upstream athttps://registry.gitlab.com, whose namespace isgitlab.com.docker.io,index.docker.ioandregistry-1.docker.ioall 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
502that says so (#1196) — an upstream that answered401with a challenge NORA cannot answer with a token (Harbor'sBasic realm="harbor"for a robot account without access to the project), or whose token server refused the credentials with401or403(Docker Hub's "incorrect username or password"), was logged as "every upstream failed" and answered with an empty502, as an outage would be. Both are now logged atwarnwith 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 with502and an OCIDENIEDbody 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. A401that persists after the token server issued a token stays a404: 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.ymlno longer cuts a docker push at 60 s (#1197) — itswebsecureentry point kept Traefik v3's defaultreadTimeout, so a layer whose upload took longer than a minute was ended with499and the push failed. The example now setstransport.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 with503, although the next configured upstream would have served it; Maven also kept one breaker for all ofmaven.proxies, so a dead first upstream opened it for the others too. Each Maven upstream now has a breaker of its own, keyedmaven:<upstream URL>like Docker, PyPI and npm (thenora_circuit_breaker_stateandnora_circuit_breaker_rejections_totalseries move fromregistry="maven"toregistry="maven:<url>"; matchregistry=~"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's404decides as before; otherwise the answer is the503withRetry-Afterit 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 throughnamespaceshas no next upstream and still gets503. - The Terraform module download goes through the circuit breaker (#1202) —
GET /terraform/v1/modules/{ns}/{name}/{provider}/{version}/downloadasked its upstream with a bare HTTP client, past theterraformcircuit breaker that every other Terraform path reports to: an open breaker did not stop module downloads (each one waited on a dead upstream and answered502), and a failing download never counted toward opening it. The download now reports to the breaker: an open breaker answers503withRetry-Afterwithout 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. A4xxis still404, and a reply withoutX-Terraform-Getis still502.terraform inithonours theRetry-Afterof a503, 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
cargodownload or a sparse-index read as much as a push, spent theupload_*bucket, whilegeneral_*only added a second limit to/api/tokens, so raisingNORA_RATE_LIMIT_GENERAL_*did nothing for the 429s of a coldcargo installordotnet restore. Now writes (PUT, POST, PATCH, DELETE) spendupload_*and reads (GET, HEAD, OPTIONS, and npm's audit POST) spendgeneral_*, 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 toauth_*alone. Thegeneral_*defaults rise from 100/s, burst 200 to 1000/s, burst 2000. Until 1.5 a read gets the larger ofgeneral_*andupload_*, setting by setting, so a configuration that keptgeneral_*belowupload_*— the helm chart's 100/200, the documented presets, anupload_*raised to cure read 429s — reads no harder than before, and NORA warns at startup; from 1.5 reads are limited bygeneral_*alone.Rate limits enforcedin the startup log now reportswrite_*andread_*in place ofupload_*andgeneral_*. - A Terraform registry module installs through NORA (#1209) — NORA answered every module download with
X-Terraform-Getrewritten to its own…/sourceendpoint, and neither kind of source survived that with the Terraform CLI: agit::source (most public modules on registry.terraform.io) was fetched from its repository address as a plain web page, which NORA served asapplication/zipand 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.gzor.zip, anarchive=parameter, or a GitHub codeload URL — is answered as…/source?archive=<kind>with its//subdirkept, and downloaded and cached through NORA as before; what the source answers is served and cached only when it is an archive (otherwise502). Agit::/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 toquay.io/org/imgreplaced the tags oforg/imgand 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/noraDocker
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.rpmChangelog
See CHANGELOG.md