Skip to content

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.


Curation answers the question: should this package be allowed into my organization?

The filter chain runs in order:

  1. Namespace filter — blocks internal package names from being proxied upstream (always active, even in off mode)
  2. Blocklist filter — blocks packages matching explicit deny rules
  3. 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.


ModeBehavior
offCuration disabled. All proxy requests pass through. Namespace filter still active.
auditFilters evaluate and log decisions, but never block. Useful for testing rules before enforcement.
enforceFilters evaluate and block matching requests with a 403 response. Requires allowlist_path to be set.
Terminal window
# Environment variable
export NORA_CURATION_MODE=audit
config.toml
[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)

The blocklist is a JSON file containing deny rules. If any rule matches a package request, it is blocked.

{
"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"
}
]
}

The registry, name, and version fields support simple glob patterns:

PatternMatches
*Everything
foo*Prefix: foo, foobar, foo-baz
*fooSuffix: 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)
exactExact string match
Terminal window
export NORA_CURATION_MODE=enforce
export NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.json
[curation]
mode = "enforce"
blocklist_path = "/etc/nora/blocklist.json"

The allowlist is a default-deny list: only packages explicitly listed are allowed through. This is the strictest form of curation.

{
"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, conan
  • name — exact package name
  • version — exact version string
  • integrity (optional) — SHA-256 hash for post-download verification
  • integrity_source (optional, informational) — upstream, local, or manual
Terminal window
export NORA_CURATION_MODE=enforce
export NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.json

When mode=enforce, the allowlist_path is required. NORA will refuse to start without it.

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.

Terminal window
export NORA_CURATION_REQUIRE_INTEGRITY=true

This protects against upstream supply chain attacks where a package is replaced after being vetted.


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.

Terminal window
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.


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.

Terminal window
export NORA_CURATION_BYPASS_TOKEN="your-secret-token"

Send the token in the X-Nora-Bypass-Token HTTP header:

Terminal window
curl -H "X-Nora-Bypass-Token: your-secret-token" \
https://registry.example.com/npm/lodash/-/lodash-4.17.22.tgz

Bypass events are logged with a [SECURITY] tag for audit purposes. Use bypass tokens sparingly and rotate them regularly.


When both files are configured, the evaluation order is:

  1. Blocklist is checked first. If a package matches a blocklist rule, it is blocked regardless of allowlist status.
  2. 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"

NORA includes CLI commands for working with curation files.

Validate a blocklist or allowlist JSON file before deploying it:

Terminal window
nora curation validate /etc/nora/blocklist.json

Output:

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 review
Terminal window
nora curation validate /etc/nora/allowlist.json

Output:

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 the curation decision for a specific package. Requires a running configuration (reads config.toml or env vars):

Terminal window
nora curation explain cargo:serde@1.0.203

The package format is registry:name@version. The version part is optional:

Terminal window
nora curation explain npm:lodash

The explain command loads the configured blocklist and allowlist files and simulates the filter chain, showing which filter produces the decision and why.


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: blocked
  • X-Nora-Rule: blocklist
  • X-Nora-Reason: CVE-2018-16492: ...

These headers allow CI/CD pipelines to detect and handle curation blocks programmatically.


Curation decisions are logged to the audit log and visible via response headers:

HeaderDescription
X-Nora-Decisionallowed or blocked
X-Nora-RuleWhich rule matched (blocklist, allowlist, cve, namespace)
X-Nora-ReasonHuman-readable reason for the decision

Use nora_http_requests_total{status="403"} in Prometheus to track blocked downloads.


{
"version": 1,
"rules": [
{
"registry": "npm",
"name": "event-stream",
"version": "3.3.6",
"reason": "CVE-2018-16492"
}
]
}

Approve only vetted packages. Everything else is denied:

Terminal window
export NORA_CURATION_MODE=enforce
export NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.json
export NORA_CURATION_REQUIRE_INTEGRITY=true

Run in audit mode first to see what would be blocked without disrupting developers:

Terminal window
export NORA_CURATION_MODE=audit
export NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.json
export NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.json

Check logs for [AUDIT] Download would be blocked entries, then switch to enforce when satisfied.

{
"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"
}
]
}