Skip to content

Upgrade Guide

1.4.3 reads 1.4.2 storage as is, on local disk and on S3, and 1.4.2 reads what 1.4.3 stores. 1.4.3 fixes the issues in GHSA-54qr-w49h-mgfq.

An OIDC provider in [[auth.oidc.providers]] without audience stops the start on 1.4.3 (exit status 78, which the packaged systemd units do not restart into), with an error naming the provider; a SIGHUP reload that brings one in is refused. Give every enabled provider the audience its CI jobs request (core.getIDToken('nora'), GitLab id_tokens: … aud:), or set any_audience = true where no nameable aud exists: it accepts any aud and warns at every start.

ChangeImpactAction required
OIDC provider without audienceRefused at start and on reload (see above)Set audience, or any_audience = true
nora import of PyPI and NuGetImported as hosted: the index, versions and dates come from what NORA holds, and no upstream is asked for the name. An imported PyPI project serves only the files imported or uploaded to itRun the import again: a PyPI or NuGet repository that 1.4.2 or older marked finished is walked again. Upload again a file that 1.4.2 or older uploaded under an imported project’s name
/health503 with failing_task when a background task keeps panicking, or when the maintenance loop finishes no tick for server.maintenance_stall_secs (300 s). A handler panic answers 500 and the server keeps serving; a panic of the audit writer ends the process with exit status 70Kubernetes restarts on the liveness probe. Under Docker without Swarm, or systemd, a 503 restarts nothing by itself: add an autoheal watcher, or a timer running nora healthcheck || systemctl restart nora
/ready503 when the store does not answer by the maintenance tick’s deadline, without a restartNone
Connection limits: server.header_read_timeout (30 s), server.keep_alive_timeout (120 s), server.max_connections (4096)Connections without a request head, and idle ones, are closed; HTTP/2 is closed with GOAWAYKeep keep_alive_timeout above the idle timeout of a reverse proxy in front (90 s or less starts with a warning), and max_connections above the number of keep-alive connections that proxy holds open
Token lifetimesAt most 3650 days on every mint route; a token minted by an OIDC identity lives no longer than its provider’s max_token_lifetime_secs or what is left of its JWT. The answer carries expires_in_secsNone
Password lockoutCounted per username at an address; an address is still held for every username after ten accounts’ worth of wrong passwordsNone
Client address on NORA_HOST=:: (the image default)An IPv4 peer is read as IPv4, so trusted_proxies and the rate limit see itNone
npm tarballsFetched only for a version the upstream’s packument lists, at its link. A redirect to another origin is followed without the upstream’s credential; metadata of a proxies entry bound to scopes comes only from its own originNone
Cache hitsCost more CPU: p95 of a large npm packument such as react about 16 ms (was about 10 ms)None

Stop 1.4.3, start 1.4.2 on the same storage. 1.4.2 does not know server.header_read_timeout, server.keep_alive_timeout, server.max_connections, server.maintenance_stall_secs and any_audience: it warns and ignores them, but under NORA_CONFIG_STRICT=true it refuses to start (exit status 78). Remove them from config.toml before the rollback, or set the [server] ones through their environment variables (NORA_HEADER_READ_TIMEOUT and the others), which 1.4.2 does not read.

1.4.0 reads 1.3.6 storage as is, on local disk and on S3. It adds keys of its own that 1.3.6 does not read: Cargo spelling records under cargo/_names/, a Cargo upstream’s index under a key of its own, and npm upstream copies under npm/<name>/upstream/. 1.4.0 fixes the issues in GHSA-5pr2-xx8w-79jc and GHSA-pg2g-wvgf-cxpp.

Some configurations that start on 1.3.6 stop on 1.4.0, each with a message naming the key:

  • a min_release_age for rpm, deb or cpan — those downloads carry no name check or release date to hold;
  • a per-registry min_release_age on an enabled proxy registry without a quarantine of that registry (curation.<registry>.quarantine or the global curation.quarantine);
  • a global min_release_age without a quarantine while the enable-set mounts proxy registries whose own flags are off, or with Hugging Face mounted by all,hf;
  • curation.require_integrity without curation.allowlist_path;
  • any [curation.hf] setting or NORA_CURATION_HF_* variable — Hugging Face models are held by repository and commit in the allowlist and blocklist.
ChangeImpactAction required
Cargo: a crate published to NORA is served only from its own entriesNo upstream is asked for it; a download of a version it does not have is 404 and nothing is fetched or storedA first cargo publish over a stored index an older release left without per-version entries is refused with 409 naming the key: delete the key if it is a cached upstream crate, or set NORA_CARGO_IMPORT_EMBEDDED=<name> if it is your own
Cargo: - and _ name one crate, as on crates.ioAnother spelling of a published crate is 404 (naming the published spelling) and never fetched; publishing it is 409None. The first start writes spelling records for crates published before 1.4.0 before it accepts requests (one listing of the published entries)
Cargo: copies of unknown versions an older release stored beside a published crateNot served for names with - or _; still listed in the UI and counted by keep_lastDelete them by hand (#1163)
Cargo retention and GCPer-version index entries and .crate files of one- and two-letter names are no longer deletedNone
Docker: a pushed tagAnswered by the pushed manifest only, also after a pull through the proxy; curation dates it by the pushA tag pushed before 1.4.0 over a tag that was already proxied stays shadowed by the proxied copy until it is pushed again
Docker: a manifest by digestAn upstream answer with another digest counts as a failed upstream; a push by digest with other bytes is 400 DIGEST_INVALIDNone
Docker: tags ending in .meta or .stats400 on every manifest routeRename such tags
Docker: a delete that cannot read the manifest metadata500, nothing removed (was 202)None
Docker: a curation refusaldocker pull and skopeo print denied: blocked by policy: …None
npm: a package published to NORAIts packument, version manifests and dist-tags are answered from its own keys and never ask an upstreamA publish over a packument of unknown origin on a package with no published versions is 409 naming the key: delete it if it is a cache, or set NORA_NPM_IMPORT_EMBEDDED=<name> if it is your package
npm: npm search and other metadata pathsAnswered by the upstream, not cachedNone
An empty upstream setting for gems, NuGet, Conan, Terraform, Ansible, CPANThe format answers from storage alone (was: the public default upstream)None, if you meant “no upstream”
PyPI: an upstream’s credentialSent only to that upstream, not to file hosts it links toNone
curation.mode left at its default with proxy registries enabledA warning on every start and nora_config_deprecated{reason="curation_mode_implicit"} 1; 1.5.0 will stop at startupSet curation.mode (NORA_CURATION_MODE) to off, audit or enforce
Client address behind a reverse proxyRead from the right of X-Forwarded-For, past the trusted proxiesCheck NORA_AUTH_TRUSTED_PROXIES if NORA sits behind more than one proxy
Hugging Face Hub proxyNew, opt-in (NORA_HF_ENABLED=true)None

Stop 1.4.0, start 1.3.6 on the same storage. Published Cargo crates, npm packages and Docker images keep working. 1.3.6 ignores the spelling records and the upstream copies 1.4.0 keeps under keys of its own, so it fetches those upstream packages again. An npm packument an older release cached for an unpublished package is never rewritten by 1.4.0, so 1.3.6 still finds it.

1.3.4 reads 1.3.3 storage as is, on local disk and on S3. No migration is needed.

ChangeImpactAction required
npm: several upstreams by scope (npm.proxies)New, opt-inNone. See npm
npm: an unreachable upstream on a cold miss502 instead of 404None. Clients report a gateway error instead of a missing package
npm audit with owned scopes or an internal namespaceA request NORA cannot strip of those names gets 4xx with the reason, instead of being forwardedUse npm 7+ (bulk audit) or yarn npm audit
Circuit breaker for Docker, PyPI, npmOne breaker per configured upstream; metric label registry="<format>:<url>"Update dashboards and alerts that match registry="pypi" etc. to registry=~"pypi(:.*)?"
circuit_breaker.overrides.dockerNow applied (was ignored)Check the values you set
Unknown keys in config.tomlLogged as a warning that names themOptionally set NORA_CONFIG_STRICT=true to refuse them
Local storage writesStaged in .nora-staging/ under the storage root; a restart removes what a crashed instance left thereNone
LogsURL credentials (user:pass@) are maskedNone

Stop 1.3.4, start 1.3.3 on the same storage. 1.3.3 serves everything 1.3.4 stored, and its retention and GC do not see the records 1.3.4 keeps under .nora-origins/ and .nora-staging/.

v0.9 is backward-compatible for all features and storage. The only action required is updating renamed environment variables if you use any of the renamed names listed below.

FeatureImpactAction required
Docker namespace isolationStorage layout adds namespace prefixNone — lazy migration is automatic
Circuit breakerNew feature, disabled by defaultNone
Docker metadata TTL + stale-while-errorNew feature, opt-inNone
Streaming read_timeoutNew feature, opt-inNone
SIGHUP hot reload for curationNew capabilityNone
linux/arm64 binaryNew platformNone

v0.9 introduces per-upstream namespace prefixes for Docker storage keys to isolate data from different upstream registries.

Old layout (v0.8):

docker/{name}/blobs/{digest}
docker/{name}/manifests/{ref}.json

New layout (v0.9):

docker/{namespace}/{name}/blobs/{digest}
docker/{namespace}/{name}/manifests/{ref}.json

The namespace is derived from the upstream URL (e.g., registry-1.docker.io becomes docker.io) or set explicitly via the namespace field in [[docker.upstreams]].

Migration is automatic and lazy:

  • When NORA serves a request, it first checks the namespaced path.
  • If not found, it falls back to the legacy flat path.
  • No background migration runs at startup.
  • Existing cached data continues to be served without interruption.
  1. Stop the running NORA instance.
  2. Replace the binary or update the container image to v0.9.
  3. Start NORA with your existing configuration.
Terminal window
# Binary upgrade
curl -fsSL https://github.com/getnora-io/nora/releases/latest/download/nora-linux-amd64 -o nora
chmod +x nora
./nora

No data migration scripts are needed.

To roll back from v0.9 to v0.8:

  1. Stop the v0.9 instance.
  2. Replace the binary or image with v0.8.
  3. Start NORA.

v0.9 adds these optional variables (all disabled or inactive by default):

VariableDefaultDescription
NORA_CB_ENABLEDfalseEnable circuit breaker
NORA_CB_THRESHOLD5Failures before opening
NORA_CB_RESET_TIMEOUT30Seconds before probing
NORA_DOCKER_READ_TIMEOUT60Per-chunk read timeout (seconds)
NORA_DOCKER_METADATA_TTL-1Metadata cache TTL in seconds
NORA_DOCKER_SERVE_STALEtrueServe cached manifests when upstream is down

See Settings for the full reference.

v0.9 shortens several variable names to follow the NORA_{SECTION}_{FIELD} convention (under 30 characters):

Old nameNew name
NORA_TF_ENABLEDNORA_TF_ENABLED
NORA_TF_PROXYNORA_TF_PROXY
NORA_TF_PROXY_TIMEOUTNORA_TF_PROXY_TIMEOUT
NORA_TF_METADATA_TTLNORA_TF_METADATA_TTL
NORA_TF_PROXY_TIMEOUT_DLNORA_TF_PROXY_TIMEOUT_DL
NORA_CONAN_PROXY_TIMEOUT_DLNORA_CONAN_PROXY_TIMEOUT_DL
NORA_CURATION_INTERNAL_NSNORA_CURATION_INTERNAL_NS

The old names are no longer recognized. Update your configuration if you use any of these.