Curation
The curation layer provides fine-grained package access control for proxy registries. It evaluates every download request against a chain of filters: blocklist, allowlist, and namespace isolation.
Overview
Section titled “Overview”Curation answers the question: should this package be allowed into my organization?
The filter chain runs in order:
- Namespace filter — blocks internal package names from being proxied upstream (always active, even in
offmode) - Blocklist filter — blocks packages matching explicit deny rules
- Allowlist filter — default-deny: only packages explicitly listed are allowed
Each filter returns one of three decisions:
- Allow — request proceeds
- Block — request is denied (403 in enforce mode)
- Skip — no opinion, pass to the next filter
If all filters skip, the request is allowed.
| Mode | Behavior |
|---|---|
off | Curation disabled. All proxy requests pass through. Namespace filter still active. |
audit | Filters evaluate and log decisions, but never block. Useful for testing rules before enforcement. |
enforce | Filters evaluate and block matching requests with a 403 response. Requires allowlist_path to be set. |
Configuration
Section titled “Configuration”# Environment variableexport NORA_CURATION_MODE=audit[curation]mode = "audit" # "off", "audit", "enforce"on_failure = "closed" # "closed" (fail-safe) or "open" (fail-open)The on_failure setting controls what happens if a filter errors or panics:
closed(default) — treat as blocked (fail-safe)open— treat as allowed (fail-open)
Blocklist
Section titled “Blocklist”The blocklist is a JSON file containing deny rules. If any rule matches a package request, it is blocked.
File format
Section titled “File format”{ "version": 1, "rules": [ { "registry": "npm", "name": "event-stream", "version": "3.3.6", "reason": "CVE-2018-16492: malicious dependency flatmap-stream" }, { "registry": "*", "name": "colors", "version": "1.4.1", "reason": "Maintainer protest: infinite loop in v1.4.1" }, { "registry": "pypi", "name": "colourama", "version": "*", "reason": "Typosquatting: malicious clone of colorama" }, { "registry": "maven", "name": "org.log4j.**", "version": "*", "reason": "Block all log4j packages pending security review" } ]}Pattern matching
Section titled “Pattern matching”The registry, name, and version fields support simple glob patterns:
| Pattern | Matches |
|---|---|
* | Everything |
foo* | Prefix: foo, foobar, foo-baz |
*foo | Suffix: foo, barfoo |
foo.** | Hierarchical prefix with dot separator: foo, foo.bar, foo.bar.baz (for Maven groupIds) |
foo/** | Hierarchical prefix with slash separator: foo, foo/bar, foo/bar/baz (for Go modules) |
exact | Exact string match |
Enable the blocklist
Section titled “Enable the blocklist”export NORA_CURATION_MODE=enforceexport NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.json[curation]mode = "enforce"blocklist_path = "/etc/nora/blocklist.json"Allowlist
Section titled “Allowlist”The allowlist is a default-deny list: only packages explicitly listed are allowed through. This is the strictest form of curation.
File format
Section titled “File format”{ "version": 1, "entries": [ { "registry": "npm", "name": "lodash", "version": "4.17.21", "integrity": "sha256:abc123def456...", "integrity_source": "upstream" }, { "registry": "npm", "name": "express", "version": "4.18.2" }, { "registry": "cargo", "name": "serde", "version": "1.0.203", "integrity": "sha256:789abc...", "integrity_source": "local" } ]}Fields:
registry— exact registry type:npm,pypi,maven,cargo,docker,go,gems,terraform,ansible,nuget,pub,conanname— exact package nameversion— exact version stringintegrity(optional) — SHA-256 hash for post-download verificationintegrity_source(optional, informational) —upstream,local, ormanual
Enable the allowlist
Section titled “Enable the allowlist”export NORA_CURATION_MODE=enforceexport NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.jsonWhen mode=enforce, the allowlist_path is required. NORA will refuse to start without it.
Integrity verification
Section titled “Integrity verification”When require_integrity=true, every allowlist entry must include an integrity hash. After downloading an artifact from upstream, NORA computes its SHA-256 and compares it against the allowlist entry. A mismatch results in a block.
export NORA_CURATION_REQUIRE_INTEGRITY=trueThis protects against upstream supply chain attacks where a package is replaced after being vetted.
Namespace Isolation
Section titled “Namespace Isolation”Namespace isolation prevents dependency confusion attacks by blocking proxy requests for packages that match internal namespace patterns. This filter is always active, even when curation mode is off.
export NORA_CURATION_INTERNAL_NAMESPACES="@mycompany/*,com.mycompany.**,github.com/myorg/**"[curation]internal_namespaces = [ "@mycompany/*", # npm scoped packages "com.mycompany.**", # Maven groupId hierarchy "github.com/myorg/**", # Go modules]When a proxy request matches an internal namespace pattern, NORA returns a 403 instead of fetching from the upstream registry. This ensures internal packages are only served from local storage.
Bypass Token
Section titled “Bypass Token”For emergency situations (e.g., a critical package update is blocked by curation), a bypass token allows specific requests to skip curation filters. Namespace isolation still applies.
export NORA_CURATION_BYPASS_TOKEN="your-secret-token"Send the token in the X-Nora-Bypass-Token HTTP header:
curl -H "X-Nora-Bypass-Token: your-secret-token" \ https://registry.example.com/npm/lodash/-/lodash-4.17.22.tgzBypass events are logged with a [SECURITY] tag for audit purposes. Use bypass tokens sparingly and rotate them regularly.
Using Both Blocklist and Allowlist
Section titled “Using Both Blocklist and Allowlist”When both files are configured, the evaluation order is:
- Blocklist is checked first. If a package matches a blocklist rule, it is blocked regardless of allowlist status.
- Allowlist is checked second. If the package is not in the allowlist, it is blocked.
This means the blocklist acts as an overlay on top of the allowlist. You can approve a package in the allowlist and then later block a specific vulnerable version via the blocklist without modifying the allowlist.
[curation]mode = "enforce"blocklist_path = "/etc/nora/blocklist.json"allowlist_path = "/etc/nora/allowlist.json"CLI Tools
Section titled “CLI Tools”NORA includes CLI commands for working with curation files.
Validate
Section titled “Validate”Validate a blocklist or allowlist JSON file before deploying it:
nora curation validate /etc/nora/blocklist.jsonOutput:
OK: Valid blocklist -- 4 rules [1] npm/event-stream@3.3.6 -- CVE-2018-16492: malicious dependency flatmap-stream [2] */colors@1.4.1 -- Maintainer protest: infinite loop in v1.4.1 [3] pypi/colourama@* -- Typosquatting: malicious clone of colorama [4] maven/org.log4j.**@* -- Block all log4j packages pending security reviewnora curation validate /etc/nora/allowlist.jsonOutput:
OK: Valid allowlist -- 3 entries (2 with integrity) [1] npm/lodash@4.17.21 [hash] [2] npm/express@4.18.2 [3] cargo/serde@1.0.203 [hash]The validate command checks:
- JSON syntax is valid
- Schema version is
1 - Required fields are present
Explain
Section titled “Explain”Explain the curation decision for a specific package. Requires a running configuration (reads config.toml or env vars):
nora curation explain cargo:serde@1.0.203The package format is registry:name@version. The version part is optional:
nora curation explain npm:lodashThe explain command loads the configured blocklist and allowlist files and simulates the filter chain, showing which filter produces the decision and why.
Blocked Response Format
Section titled “Blocked Response Format”When curation blocks a request in enforce mode, the response is a JSON 403:
{ "error": "blocked_by_policy", "error_version": "v1", "context": { "rule": "blocklist", "reason": "CVE-2018-16492: malicious dependency flatmap-stream", "registry": "npm", "package": "event-stream", "version": "3.3.6" }, "hint": "Run: nora curation explain event-stream@3.3.6", "docs": "https://docs.getnora.dev/curation"}Response headers:
X-Nora-Decision: blockedX-Nora-Rule: blocklistX-Nora-Reason: CVE-2018-16492: ...
These headers allow CI/CD pipelines to detect and handle curation blocks programmatically.
Observability
Section titled “Observability”Curation decisions are logged to the audit log and visible via response headers:
| Header | Description |
|---|---|
X-Nora-Decision | allowed or blocked |
X-Nora-Rule | Which rule matched (blocklist, allowlist, cve, namespace) |
X-Nora-Reason | Human-readable reason for the decision |
Use nora_http_requests_total{status="403"} in Prometheus to track blocked downloads.
Examples
Section titled “Examples”Block a single vulnerable package
Section titled “Block a single vulnerable package”{ "version": 1, "rules": [ { "registry": "npm", "name": "event-stream", "version": "3.3.6", "reason": "CVE-2018-16492" } ]}Allowlist-only mode (strict)
Section titled “Allowlist-only mode (strict)”Approve only vetted packages. Everything else is denied:
export NORA_CURATION_MODE=enforceexport NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.jsonexport NORA_CURATION_REQUIRE_INTEGRITY=trueAudit mode for testing
Section titled “Audit mode for testing”Run in audit mode first to see what would be blocked without disrupting developers:
export NORA_CURATION_MODE=auditexport NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.jsonexport NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.jsonCheck logs for [AUDIT] Download would be blocked entries, then switch to enforce when satisfied.
Block all packages from a namespace
Section titled “Block all packages from a namespace”{ "version": 1, "rules": [ { "registry": "maven", "name": "org.apache.log4j.**", "version": "*", "reason": "Log4Shell family - use logback instead" }, { "registry": "go", "name": "github.com/untrusted-org/**", "version": "*", "reason": "Untrusted organization" } ]}See Also
Section titled “See Also”- Configuration Reference — all environment variables
- Authentication — user management and API tokens