NORA Registry Protocol Compatibility

September 19, 2026 · View on GitHub

This document describes which parts of each registry protocol are implemented in NORA.

Legend: Full = complete implementation, Partial = basic support with limitations, Stub = placeholder, — = not implemented

Docker (OCI Distribution Spec 1.1)

EndpointMethodStatusNotes
/v2/GETFullAPI version check
/v2/_catalogGETFullList all repositories
/v2/{name}/tags/listGETFullList image tags
/v2/{name}/manifests/{ref}GETFullBy tag or digest
/v2/{name}/manifests/{ref}HEADFullCheck manifest exists
/v2/{name}/manifests/{ref}PUTFullPush manifest
/v2/{name}/manifests/{ref}DELETEFullDelete manifest
/v2/{name}/blobs/{digest}GETFullDownload layer/config
/v2/{name}/blobs/{digest}HEADFullCheck blob exists
/v2/{name}/blobs/{digest}DELETEFullDelete blob
/v2/{name}/blobs/uploads/POSTFullStart chunked upload
/v2/{name}/blobs/uploads/{uuid}PATCHFullUpload chunk
/v2/{name}/blobs/uploads/{uuid}PUTFullComplete upload
Namespaced {ns}/{name}*FullTwo-level paths
Deep paths a/b/c/name*Max 2-level (org/image)
Token auth (Bearer)FullWWW-Authenticate challenge
Cross-repo blob mountPOSTNot implemented
Referrers APIGETOCI 1.1 referrers

Proxy cache and tag freshness

  • Digest references (sha256:…) are content-addressed: cached forever, never revalidated.
  • Tag references on proxied images are revalidated against the upstream on every pull by default (docker.metadata_ttl = -1). A positive metadata_ttl (seconds) is the staleness bound: within the window the cached tag is served without an upstream round trip; after it, the next pull revalidates. 0 or negative always revalidates.
  • If every configured upstream fails and docker.serve_stale = true (default), the last cached manifest is served with x-nora-stale: true.
  • Locally pushed (hosted) images are authoritative: a pushed tag is served as-is and never revalidated against an upstream. Pushing to a name that would otherwise proxy pins that tag to the pushed copy — to keep a tag tracking the upstream, do not push to it.

Known Limitations

  • Max 2-level image path: org/image:tag works, org/sub/path/image:tag returns 404
  • Large monolithic blob PUT (>~500MB) may fail even with high body limit
  • No cross-repository blob mounting
  • Registry-mirror caching depends on Docker's image store / distribution client, not the storage driver. Docker applies the wrong credentials to mirror endpoints — it sends the upstream registry's credentials to the mirror (and on the containerd path clobbers per-mirror hosts.toml credentials), so an authenticated mirror pull fails and Docker falls back to the upstream, skipping the cache. Both the legacy and containerd code paths are affected (moby/moby#30880, #42022; upstream fix in progress in #52532). An unauthenticated mirror avoids it — set [auth] docker_anon_pull = true to serve pulls without a login while still requiring auth on push (this is the Docker-specific switch; the general anonymous_read does not open /v2/). For pull-through caching of non-Docker-Hub registries, the containerd image store additionally tags mirror requests with the ?ns= upstream namespace (containerd hosts.md). (#578)

npm

FeatureStatusNotes
Package metadata (GET)FullJSON with all versions
Scoped packages @scope/nameFullURL-encoded path
Tarball downloadFullSHA256 verified
Tarball URL rewritingFullPoints to NORA, not upstream
Publish (npm publish)FullImmutable versions
UnpublishImmutable; use quarantine/blocklist to disable a version
Dist-tags (latest, next)PartialRead from metadata, no explicit management
Search (/-/v1/search)Not implemented
Audit (bulk npm7 / audits/quick npm6)FullProxy repos: forwarded to upstream verbatim; internal-namespace names stripped/refused; anonymous-read eligible. Proxied packages only (no local advisory DB). (#597)
Upstream proxyFullConfigurable TTL

Maven

FeatureStatusNotes
Artifact download (GET)FullJAR, POM, checksums
Artifact upload (PUT)FullAny file type
GroupId path layoutFullDots → slashes
SHA1/MD5 checksumsFullStored alongside artifacts
maven-metadata.xmlPartialStored as-is, no auto-generation
SNAPSHOT versionsNo SNAPSHOT resolution
Multi-proxy fallbackFullTries proxies in order
Content-Type by extensionFull.jar, .pom, .xml, .sha1, .md5

Known Limitations

  • maven-metadata.xml not auto-generated on publish (must be uploaded explicitly)
  • No SNAPSHOT version management (-SNAPSHOT → latest timestamp)

Cargo (Sparse Index, RFC 2789)

FeatureStatusNotes
config.jsonFulldl and api fields
Sparse index lookupFullPrefix rules (1/2/3/ab/cd)
Crate downloadFull.crate files by version
cargo publishFullLength-prefixed JSON + .crate
Dependency metadataFullreq, package transforms
SHA256 verificationFullOn publish
Cache-Control headersFullimmutable for downloads, max-age=300 for index
Yank/unyankNot implemented
Owner managementNot implemented
Categories/keywordsPartialStored but not searchable

PyPI (PEP 503/691)

FeatureStatusNotes
Simple index (HTML)FullPEP 503
Simple index (JSON)FullPEP 691, via Accept header
Package versions pageFullHTML + JSON
File downloadFullWheel, sdist, egg
twine uploadFullMultipart form-data
SHA256 hashesFullIn metadata links
Case normalizationFullMy-Packagemy-package
Upstream proxyFullConfigurable TTL
JSON API metadataFullapplication/vnd.pypi.simple.v1+json
YankingNot implemented
Upload signatures (PGP)Not implemented

Go Module Proxy (GOPROXY)

FeatureStatusNotes
/@v/listFullList known versions
/@v/{version}.infoFullVersion metadata JSON
/@v/{version}.modFullgo.mod file
/@v/{version}.zipFullModule zip archive
/@latestFullLatest version info
Module path escapingFull!xX per spec
ImmutabilityFull.info, .mod, .zip immutable after first write
Size limit for .zipFullConfigurable
$GONOSUMDB / $GONOSUMCHECKNot relevant (client-side)
Upstream proxyDirect storage only

Raw File Storage

FeatureStatusNotes
Upload (PUT)FullAny file type; body streams to disk (O(frame) memory), so upload size is bounded by raw.max_file_size alone — server.body_limit_mb does not apply
Download (GET)FullContent-Type by extension
Delete (DELETE)Full
Exists check (HEAD)FullReturns size + Content-Type
Max file sizeFullConfigurable raw.max_file_size (default 100MB), enforced incrementally mid-stream
Conditional overwrite (If-Match)FullETag-based, returns 200 on success
Create-only (If-None-Match: *)FullReturns 412 if resource exists
Directory listingNot implemented
ImmutabilityFullDefault; re-upload returns 409 unless conditional headers used

RubyGems

Caching proxy for rubygems.org. Immutable gem/gemspec caching with TTL-based index refresh.

FeatureStatusNotes
Compact index (/info/{name})FullTTL-cached
Gem download (/gems/{name}-{ver}.gem)FullImmutable cache
Gemspec (/quick/Marshal.4.8/...)FullImmutable cache
Full index (specs.4.8.gz)FullTTL-cached
Latest index (latest_specs.4.8.gz)FullTTL-cached
Gem pushProxy-only (read)

Client: bundle config mirror.https://rubygems.org http://nora:4000/gems/

Terraform

Caching proxy for registry.terraform.io. Provider binaries are immutably cached; metadata uses TTL. NORA serves both Terraform protocols against the same upstream: the Registry Protocol (origin-registry, service-discovery based) and the Network Mirror Protocol (what network_mirror speaks).

FeatureStatusNotes
Service discovery (.well-known/terraform.json)FullRegistry protocol; points to NORA
Provider versions listFullTTL-cached
Provider download metadataFulldownload_url rewritten to NORA
Provider binary downloadFullImmutable cache
Module versions listFullTTL-cached
Module downloadFullX-Terraform-Get header pass-through
Network mirror — index.jsonFullMirror protocol; list versions (#801)
Network mirror — {version}.jsonFullMirror protocol; archives via NORA + zh: hash (#801)
Provider publishProxy-only (read)

Client (network mirror — Terraform requires an https: URL, trailing slash): provider_installation { network_mirror { url = "https://nora.example.com/terraform/" } }

Limitations (accepted):

  • Single upstream — the {hostname} in a mirror request is validated but not routed; only the configured terraform.proxy upstream's providers resolve.
  • Mirror-mode integrity is weaker than registry mode — in network_mirror mode Terraform does not run the origin-registry GPG check, and NORA does not itself verify SHA256SUMS.sig. Archives carry zh:<sha256> from upstream metadata (fail-closed: a platform without a resolvable shasum is omitted). Upgrade path: verify SHA256SUMS.sig against a pinned HashiCorp key at ingest.

Ansible Galaxy (v3 API)

Caching proxy for galaxy.ansible.com. Collection tarballs are immutably cached.

FeatureStatusNotes
API discoveryFull/ansible/ and /ansible/api/
Collection listFullShort v3 and Pulp-style paths
Collection detailFullURL rewriting
Collection versionsFullPaginated
Version detailFullCuration checks
Tarball downloadFullImmutable cache, both /download/ and /artifacts/ paths
Tarball curationFullBlocklist/allowlist, integrity verification
Collection publishProxy-only (read)

Namespace and collection name validation follows Galaxy spec ([a-z0-9_]+).

Client: ansible-galaxy collection install ns.name -s http://nora:4000/ansible/

NuGet (v3 API)

Caching proxy for api.nuget.org. Service index URLs are rewritten to point through NORA.

FeatureStatusNotes
Service index (/v3/index.json)Full@id URLs rewritten to NORA
Registration indexFullTTL-cached
Version list (flat container)FullTTL-cached
.nupkg downloadFullImmutable cache
.nuspec downloadFullImmutable cache
Package pushProxy-only (read)
SearchNot implemented

Client: dotnet nuget add source http://nora:4000/nuget/v3/index.json -n nora

Pub (Dart/Flutter)

Caching proxy for pub.dev. Package archives are immutably cached with SHA256 verification.

FeatureStatusNotes
Package search (/api/packages?q=)FullResponse URL rewriting
Package metadata (/api/packages/{name})Fullarchive_url rewritten to NORA
Version metadataFullCached
Security advisoriesFullCached
Archive download (.tar.gz)FullImmutable cache, SHA256 verified
Package publishProxy-only (read)

Client: export PUB_HOSTED_URL=http://nora:4000/pub && dart pub get

Conan (C/C++)

Caching proxy for ConanCenter (center2.conan.io). Recipe and package files are immutably cached (scoped to revision hashes). Metadata uses TTL-based caching.

FeatureStatusNotes
Ping (/v2/ping)FullReturns X-Conan-Server-Capabilities: revisions
Recipe searchFullProxied to upstream
Recipe latest revisionFullTTL-cached
Recipe revision listFullTTL-cached
Recipe file listFullImmutable cache (revision-scoped)
Recipe file downloadFullImmutable cache
Package latest revisionFullTTL-cached
Package revision listFullTTL-cached
Package file listFullImmutable cache (revision-scoped)
Package file downloadFullImmutable cache
Recipe/package uploadProxy-only (read)
AuthenticationAnonymous read only

Client: conan remote add nora http://nora:4000/conan

RPM (yum/dnf)

Hosted repositories with server-generated repodata (createrepo-style). Each /rpm/{repo}/ path is an independent repository; publishing or deleting a package regenerates repodata/ (repomd.xml + sha256-named primary/filelists/other.xml.gz). Package headers are parsed server-side — no createrepo_c needed on the client.

Pull-through proxy repositories: map a repo name to one upstream yum repo ([rpm.proxies] fedora = "https://…/Everything/x86_64/os"). A proxied repo is read-only (writes → 409); upstream metadata (repodata/, including the upstream's signatures) is served verbatim within rpm.metadata_ttl seconds (default 300), packages are cached forever, and a stale cache is served with x-nora-stale: true when the upstream is down. nora mirror rpm --repo <name> pre-fetches every package for fully offline (air-gapped) clients.

FeatureStatusNotes
repodata/repomd.xmlFullRegenerated on publish/delete, Cache-Control: no-cache
primary.xml.gzFullname/evr/arch, provides/requires/conflicts/obsoletes with flags, primary file subset, header-range
filelists.xml.gzFullAll files with dir/ghost types
other.xml.gzFullChangelogs (last 10 per package, configurable)
Package publish (PUT {repo}/{name}.rpm)FullHeader parsed and validated; invalid RPMs rejected
Package delete (DELETE {repo}/{name}.rpm)FullRepodata regenerated
Package downloadFullByte-identical, sha256 pkgid in repodata
GPG-signed repodata (repomd.xml.asc)FullSigned on every regeneration; verify with repo_gpgcheck=1. Object-store backends (S3/GCS) require signing.key_path — without it signing is disabled with a startup warning
Public key (repodata/repomd.xml.key)FullArmored, for gpgkey=; key auto-generated at first boot
Package signatures (gpgcheck=1)Packages are stored as uploaded; NORA signs metadata, not packages
Reconcile (POST {repo}/-/reindex)FullHeals out-of-band storage changes: drops orphan metadata, adopts added packages, rebuilds + re-signs repodata
Upstream proxyFullPer-repo pull-through via [rpm.proxies]; digest quarantine via [curation.rpm]
Offline mirrorFullnora mirror rpm --repo <name> [--arch x86_64,noarch] warms the full package set
sqlite metadata (*_db)XML metadata only (all modern dnf/yum versions)
Delta RPMs (prestodelta)Not generated
Module metadata (modules.yaml)Not generated

Publish: curl -u user:pass -T pkg.rpm http://nora:4000/rpm/myrepo/pkg.rpm

Client .repo file:

[nora-myrepo]
name=NORA myrepo
baseurl=http://nora:4000/rpm/myrepo
enabled=1
gpgcheck=0
repo_gpgcheck=1
gpgkey=http://nora:4000/rpm/myrepo/repodata/repomd.xml.key

gpgcheck=0 stays: NORA signs the repository metadata, not the packages themselves. With signing disabled (signing.enabled = false), also set repo_gpgcheck=0.

Debian (APT)

Hosted repositories with server-generated indexes, in either apt layout (chosen per package at publish): flat (indexes at the repo root, sources line deb <url>/deb/{repo} ./) or structured (dists/{distribution}/ suites with components, sources line deb <url>/deb/{repo} jammy main). Each /deb/{repo}/ path is an independent repository; publishing or deleting a package regenerates every affected index. Package control paragraphs are parsed server-side from the .deb (ar → control.tar.{,gz,xz,zst}) — no dpkg-scanpackages needed.

Pull-through proxy repositories: map a repo name to one upstream apt repo ([deb.proxies] debian = "https://deb.debian.org/debian"). A proxied repo is read-only (writes → 409); upstream indexes (dists/, including the upstream's InRelease/Release.gpg) are served verbatim within deb.metadata_ttl seconds (default 300), packages are cached forever, and a stale cache is served with x-nora-stale: true when the upstream is down. nora mirror deb --repo <name> --dist <dist> pre-fetches every package for fully offline (air-gapped) clients.

FeatureStatusNotes
Packages / Packages.gzFullVerbatim control paragraphs + Filename/Size/MD5sum/SHA1/SHA256
Release (flat repo)FullMD5Sum + SHA256 sections, Cache-Control: no-cache
dists/ layout (suites/components)FullPUT ...?distribution=<dist>[&component=<comp>]; per-dist Release + {component}/binary-{arch}/Packages{,.gz}; arch-all packages folded into every concrete arch index; upload path is free-form (Filename is repo-root-relative, pool/ not required)
Package publish (PUT {repo}/{name}.deb)FullControl parsed and validated; invalid debs rejected; decompression bombs bounded
Package delete (DELETE {repo}/{name}.deb)FullIndexes regenerated; emptied distributions are removed
Package downloadFullByte-identical
InRelease (clearsigned) / Release.gpg (detached)FullSigned on every regeneration (per distribution in the structured layout); verify via signed-by
Public key (pubkey.gpg)FullArmored, for the signed-by keyring; key auto-generated at first boot
Reconcile (POST {repo}/-/reindex)FullHeals out-of-band storage changes: drops orphan metadata, adopts added packages (into the flat layout — placement is upload-time intent), rebuilds + re-signs indexes
by-hashNot generated; index files are served Cache-Control: no-cache
Translations / Contents indexesNot generated (proxied repos pass them through)
Upstream proxyFullPer-repo pull-through via [deb.proxies]; digest quarantine via [curation.deb]
Offline mirrorFullnora mirror deb --repo <name> [--dist <dist>] [--component main] [--arch amd64] warms the full package set

Publish (flat): curl -u user:pass -T pkg.deb http://nora:4000/deb/myrepo/pkg.deb

Publish (structured): curl -u user:pass -T pkg.deb "http://nora:4000/deb/myrepo/pool/pkg.deb?distribution=jammy&component=main"

Client setup:

curl -fsSL http://nora:4000/deb/myrepo/pubkey.gpg -o /etc/apt/keyrings/nora.asc
# flat
echo "deb [signed-by=/etc/apt/keyrings/nora.asc] http://nora:4000/deb/myrepo ./" \
  > /etc/apt/sources.list.d/nora.list
# structured
echo "deb [signed-by=/etc/apt/keyrings/nora.asc] http://nora:4000/deb/myrepo jammy main" \
  > /etc/apt/sources.list.d/nora.list

With signing disabled (signing.enabled = false), use [trusted=yes] instead of [signed-by=...].

CPAN

Caching proxy for www.cpan.org. Distribution tarballs are immutably cached; the package index uses TTL-based caching.

FeatureStatusNotes
Package index (02packages.details.txt.gz)FullTTL-cached
Module list (03modlist.data.gz)FullTTL-cached
Author directory (01mailrc.txt.gz)FullTTL-cached
Distribution download (authors/id/*)FullImmutable cache
Distribution publishProxy-only (read)

The web UI groups cached releases by parsing archive filenames with a local adaptation of CPAN::DistnameInfo. It supports modern Dist-1.23 names and legacy underscore, dot, and directly-attached version forms. .meta and .readme sidecars are not fetched or parsed for grouping. Consequently, the distribution page lists versions already cached by NORA; unparseable archive names are omitted from the grouped author view. The exact rules and examples are documented in the CPAN proxy design.

Client:

cpanm --from http://nora:4000/cpan Module::Name

Helm OCI

Helm charts are stored as OCI artifacts via the Docker registry endpoints. helm push and helm pull work through the standard /v2/ API.

FeatureStatusNotes
helm push (OCI)FullVia Docker PUT manifest/blob
helm pull (OCI)FullVia Docker GET manifest/blob
Helm repo index (index.yaml)Not implemented (OCI only)

Cross-Cutting Features

FeatureStatusNotes
Authentication (Bearer/Basic)FullPer-request token validation; see OIDC note below
Anonymous readFullNORA_AUTH_ANONYMOUS_READ=true
Rate limiting (429 + Retry-After)Fulltower_governor, per-IP, documented in OpenAPI
405 Method Not Allowed + AllowFullRFC 9110 §15.5.6, multi-method routes return Allow header
Prometheus metricsFull/metrics endpoint
Health checkFull/health
Swagger/OpenAPIFull/api-docs
S3 backendFullAWS S3, Ceph RGW. Basic storage works on any S3-compatible; multi-replica write-serialization has a caveat — see note below.
GCS backendFullNative Google Cloud Storage (storage.mode = "gcs"): Workload Identity / service-account JSON / ambient credentials; endpoint override for emulators and Private Google Access. Same single-writer caveat as S3 for rpm/deb publishing (in-process publish lock). Hash-pinning (at-rest integrity verification) works on every backend: the pin is the sha256 object metadata on S3/GCS.
Local filesystem backendFullDefault, content-addressable
Activity logFullRecent push/pull in dashboard
Backup/restoreFullCLI commands
Mirror CLIFullnora mirror for npm/pip/cargo/maven/docker/rpm/deb

OIDC authentication transport

OIDC JWT tokens are validated only on the Bearer authentication path. Docker, twine, and Maven clients transmit credentials via HTTP Basic, and the Basic-auth handler recognises htpasswd passwords and opaque API tokens (nra_…) but does not attempt OIDC JWT validation on the password field. To use OIDC workload identity from CI, send the JWT as a Bearer token (e.g. Authorization: Bearer <jwt>) rather than as a docker login password. For docker login specifically, use an opaque API token created via the UI or /api/tokens. (#853)

Storage backend notes

  • Multi-replica write-serialization needs conditional-write/CAS. NORA's write lock (publish_lock) is safe under a single writer — one replica, or an RWO volume single-mounted so only one pod writes. Serializing concurrent writes across multiple replicas requires an object store with atomic conditional-write / compare-and-swap: AWS S3 and Ceph RGW provide it; Garage (no consensus layer) and SeaweedFS (immature) do not — on those, run single-writer. Not every "S3-compatible" backend is equivalent here.