Курирование
Слой курирования обеспечивает детальный контроль доступа к пакетам для прокси-реестров. Он оценивает каждый запрос на скачивание по цепочке фильтров (изоляция пространств имён, список блокировки, список разрешений, минимальный возраст релиза) и может придерживать только что полученные артефакты в карантине дайджестов.
Курирование отвечает на вопрос: должен ли этот пакет быть допущен в мою организацию?
Запрос на скачивание проверяется в таком порядке:
- Изоляция пространств имён — не даёт получать внутренние имена пакетов из внешнего реестра (всегда активна, даже в режиме
off) - В режиме
offна этом запрос разрешается - Токен обхода — запрос с токеном пропускает фильтры ниже
- Список блокировки — блокирует пакеты, соответствующие явным правилам запрета
- Список разрешений — запрет по умолчанию: допускаются только явно указанные пакеты
- Минимальный возраст релиза — блокирует пакеты моложе порога
Каждый фильтр возвращает одно из трёх решений:
- 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 variableexport NORA_CURATION_MODE=audit[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=enforceexport 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=enforceexport 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=7dexport NORA_CURATION_QUARANTINE=enforceexport 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 ownquarantine = "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=3dexport NORA_CURATION_PYPI_MIN_RELEASE_AGE=14dexport NORA_CURATION_CARGO_MIN_RELEASE_AGE=1dexport NORA_CURATION_GO_MIN_RELEASE_AGE=7dexport 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, enforceexport 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. Используйте токены обхода осторожно и регулярно выполняйте их ротацию.
Совместное использование списка блокировки и списка разрешений
Заголовок раздела «Совместное использование списка блокировки и списка разрешений»Когда оба файла настроены, порядок проверки следующий:
- Список блокировки проверяется первым. Если пакет соответствует правилу списка блокировки, он блокируется независимо от наличия в списке разрешений.
- Список разрешений проверяется вторым. Если пакета нет в списке разрешений, он блокируется.
Это означает, что список блокировки действует как наложение поверх списка разрешений. Вы можете одобрить пакет в списке разрешений, а затем заблокировать конкретную уязвимую версию через список блокировки без изменения списка разрешений.
[curation]mode = "enforce"blocklist_path = "/etc/nora/blocklist.json"allowlist_path = "/etc/nora/allowlist.json"Инструменты командной строки
Заголовок раздела «Инструменты командной строки»NORA включает CLI-команды для работы с файлами курирования.
Проверка (validate)
Заголовок раздела «Проверка (validate)»Проверьте 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 reviewnora 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) - Наличие обязательных полей
Объяснение (explain)
Заголовок раздела «Объяснение (explain)»Объясняет решение курирования для конкретного пакета. Требует работающей конфигурации (читает 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.6Package: npm:event-stream@3.3.6Decision: 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: blockedX-Nora-Rule: blocklistX-Nora-Reason: CVE-2018-16492: ...
В hint нет реестра; добавьте его, когда запускаете команду: nora curation explain npm:event-stream@3.3.6.
Эти заголовки позволяют CI/CD-пайплайнам обнаруживать и обрабатывать блокировки курирования программным путём. Задержка карантином имеет свой формат ответа.
Наблюдаемость
Заголовок раздела «Наблюдаемость»Заголовки X-Nora-* ставятся на отклонённые запросы; у разрешённого скачивания их нет.
| Заголовок | Описание |
|---|---|
X-Nora-Decision | blocked |
X-Nora-Rule | Какое правило сработало: blocklist, allowlist, namespace, min-release-age |
X-Nora-Reason | Человекочитаемая причина решения |
X-Nora-Quarantine | pending при задержке карантином |
Каждое отклонённое скачивание записывается в журнал аудита как 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=enforceexport NORA_CURATION_ALLOWLIST_PATH=/etc/nora/allowlist.jsonexport NORA_CURATION_REQUIRE_INTEGRITY=trueРежим аудита для тестирования
Заголовок раздела «Режим аудита для тестирования»Сначала запустите в режиме аудита, чтобы увидеть, что будет заблокировано, не мешая разработчикам:
export NORA_CURATION_MODE=auditexport NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.jsonexport 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" } ]}Смотрите также
Заголовок раздела «Смотрите также»- Справочник по конфигурации — все переменные окружения
- Аутентификация — управление пользователями и API-токены
- Журнал аудита — записи
curation_block - Dozor — строит список блокировки из OSV