Перейти к содержимому

Курирование

Слой курирования обеспечивает детальный контроль доступа к пакетам для прокси-реестров. Он оценивает каждый запрос на скачивание по цепочке фильтров (изоляция пространств имён, список блокировки, список разрешений, минимальный возраст релиза) и может придерживать только что полученные артефакты в карантине дайджестов.


Курирование отвечает на вопрос: должен ли этот пакет быть допущен в мою организацию?

Запрос на скачивание проверяется в таком порядке:

  1. Изоляция пространств имён — не даёт получать внутренние имена пакетов из внешнего реестра (всегда активна, даже в режиме off)
  2. В режиме off на этом запрос разрешается
  3. Токен обхода — запрос с токеном пропускает фильтры ниже
  4. Список блокировки — блокирует пакеты, соответствующие явным правилам запрета
  5. Список разрешений — запрет по умолчанию: допускаются только явно указанные пакеты
  6. Минимальный возраст релиза — блокирует пакеты моложе порога

Каждый фильтр возвращает одно из трёх решений:

  • Allow — запрос продолжается
  • Block — запрос отклоняется (403 в режиме enforce)
  • Skip — нет решения, передать следующему фильтру

Решает первый Allow или Block. Если все фильтры вернули Skip, запрос разрешается.

Карантин дайджестов — отдельная проверка: она срабатывает после того, как артефакт получен из внешнего реестра, и до того, как он отдан клиенту.


РежимПоведение
offКурирование отключено. Все прокси-запросы проходят. Изоляция пространств имён по-прежнему активна.
auditФильтры оценивают и логируют решения, но никогда не блокируют. Полезно для тестирования правил перед применением.
enforceФильтры оценивают и блокируют совпадающие запросы ответом 403. Нужен хотя бы один активный механизм (см. ниже).

В режиме enforce NORA не запускается (код выхода 78), если не задано хотя бы одно из: allowlist_path, blocklist_path, min_release_age, quarantine, internal_namespaces, require_integrity. Список разрешений не обязателен.

Окно терминала
# Environment variable
export NORA_CURATION_MODE=audit
config.toml
[curation]
mode = "audit" # "off", "audit", "enforce"

Если фильтр завершается ошибкой, запрос блокируется (безопасный отказ). Параметр on_failure принимает только "closed" — значение по умолчанию; "open" не реализован, и с ним NORA не запускается.


Список блокировки — это JSON-файл, содержащий правила запрета. Если любое правило совпадает с запросом пакета, он блокируется.

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

Поля registry, name и version поддерживают простые glob-паттерны:

ПаттернСовпадения
*Всё
foo*Простой префикс (starts_with): foo, foobar, foo-baz. Не учитывает разделители — com.acme* совпадает и с com.acmecorp. Для изоляции пространств имён используйте .** / /** ниже.
*fooСуффикс: foo, barfoo
foo.**Иерархический префикс с разделителем-точкой или Maven-двоеточием: foo, foo.bar, foo.bar.baz, а также foo:artifact (Maven groupId:artifactId)
foo/**Иерархический префикс с разделителем-слешем: foo, foo/bar, foo/bar/baz (модули Go, Terraform ns/type)
exactТочное совпадение строки
Окно терминала
export NORA_CURATION_MODE=enforce
export NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.json
[curation]
mode = "enforce"
blocklist_path = "/etc/nora/blocklist.json"

NORA не держит базу уязвимостей (ADR-2), поэтому блоклист — это ваш файл. Поддерживать его руками против тысяч записей в неделю невозможно, и для этого есть Dozor: он собирает фид OSV и опись того, что лежит в реестре, ровно в этот файл. Менять в NORA ничего не нужно — только blocklist_path.


Список разрешений — это список с запретом по умолчанию: допускаются только явно указанные пакеты. Это самая строгая форма курирования.

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

Поля:

  • registry — тип реестра: npm, pypi, maven, cargo, docker, go, gems, terraform, ansible, nuget, pub, conan, cpan, hf (в name — id репозитория, в version — коммит)
  • name — точное имя пакета
  • version — точная строка версии

Имена и версии сравниваются в той форме, в какой их приводит сам реестр (например, имена по PEP 503 для PyPI и идентификаторы в нижнем регистре для NuGet), поэтому запись срабатывает при любом написании пакета клиентом. То же относится к правилам списка блокировки и к внутренним пространствам имён.

  • integrity (необязательно) — хеш SHA-256 для проверки после скачивания
  • integrity_source (необязательно, информационное) — upstream, local или manual
Окно терминала
export NORA_CURATION_MODE=enforce
export NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.json

Если в режиме enforce задан allowlist_path, файл должен существовать, иначе NORA не запускается (код выхода 78).

Когда require_integrity=true, каждая запись списка разрешений должна содержать хеш integrity. После скачивания артефакта из вышестоящего реестра NORA вычисляет его SHA-256 и сравнивает со значением из записи списка разрешений. При несовпадении запрос блокируется.

Окно терминала
export NORA_CURATION_REQUIRE_INTEGRITY=true

Это защищает от атак на цепочку поставок, при которых пакет заменяется после проверки.


Изоляция пространств имён предотвращает атаки подмены зависимостей (dependency confusion), блокируя прокси-запросы для пакетов, соответствующих паттернам внутренних пространств имён. Этот фильтр всегда активен, даже когда режим курирования установлен в off.

Окно терминала
export NORA_CURATION_INTERNAL_NS="@mycompany/*,com.mycompany.**,github.com/myorg/**"
[curation]
internal_namespaces = [
"@mycompany/*", # npm scoped packages
"com.mycompany.**", # Maven groupId hierarchy
"github.com/myorg/**", # Go modules
]

Когда запрос совпадает с паттерном внутреннего пространства имён, NORA никогда не запрашивает его у внешнего реестра. Пакет, опубликованный в NORA под внутренним именем, отдаётся из локального хранилища; без локальной копии запрос отклоняется с 403 (X-Nora-Rule: namespace), а Docker отвечает 404 MANIFEST_UNKNOWN.


Блокирует пакеты, опубликованные менее заданного времени назад. Это защищает от атак на цепочку поставок, использующих окно между публикацией пакета и его проверкой сообществом.

Окно терминала
export NORA_CURATION_MIN_RELEASE_AGE=7d
export NORA_CURATION_QUARANTINE=enforce
export NORA_CURATION_QUARANTINE_TTL=7d
[curation]
min_release_age = "7d"
quarantine = "enforce"
quarantine_ttl = "7d"

Длительность задаётся в h (часы), d (дни) и w (недели), единицы можно сочетать: 12h, 7d, 2w, 1w2d. Другие единицы, например 30m, не принимаются: при mode = "enforce" NORA не запускается, в остальных режимах пишет в лог Invalid min_release_age и работает без фильтра.

Разные реестры могут требовать разных порогов. Значение для реестра действует на свой реестр — и с глобальным min_release_age, и без него (с NORA 1.4.0; раньше значение реестра действовало только рядом с глобальным). Глобальное значение — по умолчанию для реестров без своего, а своё значение реестра важнее глобального.

На включённом прокси-реестре значению реестра нужен карантин этого реестра: его [curation.<registry>] quarantine или глобальный quarantine. Карантин другого реестра не считается — он не держит загрузки этого реестра; без карантина NORA не стартует и называет ключ:

[curation]
min_release_age = "7d" # default for registries without their own
quarantine = "enforce"
[curation.npm]
min_release_age = "3d" # npm packages: 3 days
[curation.pypi]
min_release_age = "14d" # PyPI packages: 14 days
[curation.cargo]
min_release_age = "1d" # Cargo crates: 1 day

Переменные окружения следуют шаблону NORA_CURATION_{REGISTRY}_MIN_RELEASE_AGE:

Окно терминала
export NORA_CURATION_NPM_MIN_RELEASE_AGE=3d
export NORA_CURATION_PYPI_MIN_RELEASE_AGE=14d
export NORA_CURATION_CARGO_MIN_RELEASE_AGE=1d
export NORA_CURATION_GO_MIN_RELEASE_AGE=7d
export NORA_CURATION_DOCKER_MIN_RELEASE_AGE=2d

Секция для отдельного реестра есть у всех реестров, кроме Raw и Hugging Face (всего 15): npm, pypi, cargo, go, docker, maven, gems, terraform, ansible, nuget, pub (Dart), conan, rpm, deb, cpan. Для rpm и deb действуют только настройки карантина: их проксирующие репозитории проверяет карантин дайджестов, а не фильтры по именам. min_release_age для rpm, deb или cpan при старте отвергается: применить его нельзя, а у загрузок cpan нет даты релиза. Для них задайте quarantine. Значение для невключённого реестра ничего не держит: NORA стартует, предупреждает и не считает его активным контролем в режиме enforce.

Фильтр оценивает возраст пакета по дате публикации и срабатывает на скачивании артефакта, а не на запросе метаданных:

  • С NORA_TRUST_UPSTREAM_DATES=true (server.trust_upstream_dates) он берёт дату релиза, которую сообщает внешний реестр, если она есть: npm, PyPI, Cargo, Go, NuGet, Conan, pub.dev, Maven, RubyGems, Ansible и Terraform. Даты внешних реестров не подписаны, поэтому это включается явно.
  • Иначе он берёт время, когда NORA закешировала метаданные пакета, если оно есть.
  • Если дата неизвестна, фильтр возвращает Skip, и решение остаётся за карантином дайджестов.

Блокировка называет правило и возраст, например "rule":"min-release-age","reason":"package is 0h old (minimum: 7d)".

Фильтр работает только в режиме mode = "audit" или mode = "enforce".


Карантин дайджестов придерживает артефакт, только что полученный NORA из внешнего реестра, пока он не будет известен заданное время. Он опирается на дайджест содержимого и собственные часы NORA, поэтому не нуждается в дате внешнего реестра и не может быть ею обманут. Карантин действует на всех прокси-реестрах.

Окно терминала
export NORA_CURATION_QUARANTINE=enforce # off (default), observe, enforce
export NORA_CURATION_QUARANTINE_TTL=7d # default 14d
[curation]
quarantine = "enforce"
quarantine_ttl = "7d"
[curation.docker]
quarantine_ttl = "2d" # per-registry override
РежимПоведение
offКарантина нет (по умолчанию)
observeЗапоминает дайджесты при первом появлении и логирует, что было бы задержано, но отдаёт всё
enforceОтвечает 403 на артефакт, пока ему не исполнится quarantine_ttl
  • quarantine_ttl принимает единицы h, d и w, как min_release_age; значение, которое NORA не может разобрать, заменяется значением по умолчанию 14d.
  • Переопределение для реестра: [curation.<registry>] quarantine / quarantine_ttl или NORA_CURATION_<REGISTRY>_QUARANTINE / _QUARANTINE_TTL.
  • С NORA_TRUST_UPSTREAM_DATES=true время первого появления артефакта отсчитывается от даты релиза во внешнем реестре, если реестр её сообщает: давно выпущенный пакет отдаётся сразу, задерживается только свежий.

Задержанный артефакт получает 403 с заголовком X-Nora-Quarantine: pending:

{
"error": "quarantine",
"message": "held by quarantine policy on registry 'npm': artifact must age past the quarantine window before it is served",
"detail": {
"registry": "npm",
"digest": "sha256:7b75c105...",
"policy": { "control": "quarantine", "quarantine_ttl_secs": 604800 },
"quarantine_until": 1792489967,
"remaining_secs": 604800
}
}

Для экстренных ситуаций (например, критическое обновление пакета заблокировано курированием) токен обхода позволяет определённым запросам пропускать фильтры курирования. Изоляция пространств имён по-прежнему применяется.

Окно терминала
export NORA_CURATION_BYPASS_TOKEN="your-secret-token"

Отправляйте токен в HTTP-заголовке X-Nora-Bypass-Token:

Окно терминала
curl -H "X-Nora-Bypass-Token: your-secret-token" \
https://registry.example.com/npm/lodash/-/lodash-4.17.22.tgz

Каждый обход пишется в лог предупреждением [SECURITY] Curation bypassed via token. Используйте токены обхода осторожно и регулярно выполняйте их ротацию.


Совместное использование списка блокировки и списка разрешений

Заголовок раздела «Совместное использование списка блокировки и списка разрешений»

Когда оба файла настроены, порядок проверки следующий:

  1. Список блокировки проверяется первым. Если пакет соответствует правилу списка блокировки, он блокируется независимо от наличия в списке разрешений.
  2. Список разрешений проверяется вторым. Если пакета нет в списке разрешений, он блокируется.

Это означает, что список блокировки действует как наложение поверх списка разрешений. Вы можете одобрить пакет в списке разрешений, а затем заблокировать конкретную уязвимую версию через список блокировки без изменения списка разрешений.

[curation]
mode = "enforce"
blocklist_path = "/etc/nora/blocklist.json"
allowlist_path = "/etc/nora/allowlist.json"

NORA включает CLI-команды для работы с файлами курирования.

Проверьте JSON-файл списка блокировки или списка разрешений перед развёртыванием:

Окно терминала
nora curation validate /etc/nora/blocklist.json

Вывод:

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
Окно терминала
nora curation validate /etc/nora/allowlist.json

Вывод:

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]

Команда validate проверяет:

  • Корректность синтаксиса JSON
  • Версию схемы (должна быть 1)
  • Наличие обязательных полей

Объясняет решение курирования для конкретного пакета. Требует работающей конфигурации (читает config.toml или переменные окружения):

Окно терминала
nora curation explain cargo:serde@1.0.203

Формат пакета: registry:name@version; префикс реестра обязателен, часть с версией — нет:

Окно терминала
nora curation explain npm:lodash

Команда explain загружает настроенные список блокировки, список разрешений, внутренние пространства имён и минимальный возраст релиза, моделирует цепочку фильтров и показывает, какой фильтр принял решение и почему (со списком блокировки выше):

Окно терминала
nora curation explain npm:event-stream@3.3.6
Package: npm:event-stream@3.3.6
Decision: Block { rule: "blocklist", reason: "CVE-2018-16492: malicious dependency flatmap-stream" }
Decided by: blocklist

Когда курирование блокирует запрос в режиме enforce, ответ представляет собой 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"
}

Заголовки ответа:

  • X-Nora-Decision: blocked
  • X-Nora-Rule: blocklist
  • X-Nora-Reason: CVE-2018-16492: ...

В hint нет реестра; добавьте его, когда запускаете команду: nora curation explain npm:event-stream@3.3.6.

Эти заголовки позволяют CI/CD-пайплайнам обнаруживать и обрабатывать блокировки курирования программным путём. Задержка карантином имеет свой формат ответа.


Заголовки X-Nora-* ставятся на отклонённые запросы; у разрешённого скачивания их нет.

ЗаголовокОписание
X-Nora-Decisionblocked
X-Nora-RuleКакое правило сработало: blocklist, allowlist, namespace, min-release-age
X-Nora-ReasonЧеловекочитаемая причина решения
X-Nora-Quarantinepending при задержке карантином

Каждое отклонённое скачивание записывается в журнал аудита как curation_block (в detail — правило и причина). В режиме audit несостоявшаяся блокировка пишется в лог как [AUDIT] Download would be blocked.

Метрики Prometheus: nora_curation_decisions_total{decision}, nora_quarantine_holds_total{registry,outcome} и nora_namespace_isolation_refused_total{registry} (см. Метрики Prometheus).


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

Режим только со списком разрешений (строгий)

Заголовок раздела «Режим только со списком разрешений (строгий)»

Одобряйте только проверенные пакеты. Всё остальное запрещено:

Окно терминала
export NORA_CURATION_MODE=enforce
export NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.json
export NORA_CURATION_REQUIRE_INTEGRITY=true

Сначала запустите в режиме аудита, чтобы увидеть, что будет заблокировано, не мешая разработчикам:

Окно терминала
export NORA_CURATION_MODE=audit
export NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.json
export NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.json

Проверьте логи на наличие записей [AUDIT] Download would be blocked, затем переключитесь на enforce, когда будете удовлетворены результатом.

Блокировка всех пакетов из пространства имён

Заголовок раздела «Блокировка всех пакетов из пространства имён»
{
"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"
}
]
}