19 KiB
ADR-001: Security-гейт — secret-scanning + dependency audit перед мержем
- Статус: Accepted (proposed → принято архитектором ORCH-022)
- Дата: 2026-06-07
- Задача: ORCH-022
- Связанный global ADR:
docs/architecture/adr/adr-0012-security-gate.md - Источники:
01-brd.md(BR-1..BR-14),02-trz.md(FR-1..FR-7, §4 варианты, §7 инварианты),03-acceptance-criteria.md(AC-1..AC-21).
Контекст
Оркестратор автономен: developer-агент пишет код без человека-фильтра. Перед слиянием
ветки задачи в main нет автоматической проверки на утёкший секрет (ключ/токен/пароль/
приватный ключ) и на уязвимую зависимость (известная CVE). Для self-hosting это особенно
опасно: один общий прод-инстанс обслуживает все проекты с общей БД — секрет или CVE,
просочившийся через одну задачу, попадает в прод всех проектов (CLAUDE.md §self-hosting, §8).
Конвейер уже содержит линию детерминированных страховок на ребре deploy-staging → deploy
(непосредственно перед фактическим мержем PR в main, который делает deployer в начале
стадии deploy):
- merge-gate (ORCH-043,
check_branch_mergeable) — догонmain+ re-test + сериализация; - image-freshness (ORCH-058,
check_staging_image_fresh) — провенанс staging-образа.
Оба построены по одному паттерну: leaf-модуль чистой логики (never-raise) + тонкая обёртка
в QG_CHECKS + врезка-обработчик _handle_* в advance_stage, с условным раскатом
(*_enabled + *_repos, реально только для self-hosting при пустом scope) и откатом на
development с developer-retry (cap MAX_DEVELOPER_RETRIES = 3).
Открытые вопросы BRD §8 / TRZ §4, требующие решения архитектора:
- Размещение гейта в пайплайне (review / merge-edge / CI-job).
- Где запускается сканер (CI-job через
check_ci_green/ отдельный QG-чек). - Degrade при недоступном CVE-фиде (fail-open / fail-closed).
- Выбор инструментов (gitleaks/trufflehog; pip-audit/trivy).
Решение
Р-1. Размещение — Вариант M (под-гейт ребра deploy-staging → deploy), ПЕРВЫМ среди edge-под-гейтов
Security-гейт реализуется как детерминированный под-гейт того же ребра
deploy-staging → deploy, что merge-gate и image-freshness, и исполняется ПЕРВЫМ —
ДО merge-gate. STAGE_TRANSITIONS не меняется (триггер — то же событие «staging-
deployer завершился»; инвариант TRZ §7.1).
Порядок врезок в advance_stage (блок current_stage == "deploy-staging"):
check_staging_status (PASS, существующий QG стадии)
→ security-gate (НОВЫЙ, _handle_security_gate) ← первым
→ merge-gate (_handle_merge_gate)
→ image-freshness (_handle_image_freshness)
→ Phase A (self-deploy approve)
Почему merge-edge, а не review (Вариант R):
- BRD-требование «перед слиянием в
main» удовлетворяют оба, но на review-стадии diff может разойтись с тем, что реально вольётся вmain(параллельная задача двигаетmainвперёд между review и merge). Merge-edge — последняя точка перед фактическим мержем. - Переиспользуется готовая машинерия отката/retry/нотификаций edge-под-гейтов (минимальный blast-radius, инвариант TRZ §7).
Почему ПЕРВЫМ (до merge-gate), а не после image-freshness:
- Дёшево фейлить. merge-gate (rebase + re-test, минуты) и image-freshness (docker rebuild, до 1200с) — дорогие. Нет смысла гонять их на ветке с секретом/CVE.
- Корректность для секретов. Секрет живёт в собственных коммитах ветки;
rebase онто
mainего не добавляет и не убирает → скан диапазонаorigin/main..HEADдо rebase ловит ровно те коммиты, что попадут вmain. - Анти-петля для зависимостей. Аудит ветки до rebase оценивает то, что вносит
ИМЕННО эта задача (её
requirements.txt/diff), а не уязвимость, которую притащил в ветку обновившийсяmain. Аудит после rebase «обвинял» бы задачу в чужой (main'овой) CVE → ложный откат→ development→ петля (прецедент ORCH-061). Скан до rebase этого избегает. - Проще, чем image-freshness. Гейт исполняется ДО захвата merge-lease → при FAIL
lease освобождать не нужно (в отличие от
_handle_image_freshness). Чистый откат.
Почему не CI-job (Вариант C): пороги severity, warning-vs-block, аллоулист и машиночитаемый артефакт-вердикт плохо выражаются одним статусом коммита Gitea; путь коуплится с CI-раннером. Отклонено для v1; оставлено как точка расширения (BR-14).
Р-2. Инструменты
- Secret-scanning —
gitleaks. Полностью offline (без сетевого фида → гарантия «секрет всегда блокирует» не зависит от сети, BR-2), один статический бинарь, детерминированный, конфиг + аллоулист в репо (.gitleaks.toml, BR-13), поддержка--log-opts="origin/main..HEAD"(скан диапазона), JSON-отчёт, exit-code контракт (0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента). Бинарь устанавливается вDockerfile(Go-бинарь, не pip-пакет) — см.07-infra-requirements.md. - Dependency audit —
pip-audit. Python-native (v1-стек — сам оркестратор, Python), читаетrequirements.txt, источник advisory — OSV/PyPI, JSON-выход, ставится черезrequirements.txt. trivy/trufflehog отклонены как тяжелее/контейнер-ориентированные для v1-цели «Python-only» (A3).
Конкретные инструменты — деталь реализации; контракт гейта (вход: repo/branch/wi,
выход: (bool, reason) + артефакт) от них не зависит, заменяемы за leaf-модулем.
Р-3. Degrade при недоступном CVE-фиде — fail-open + громкий warning (дефолт)
pip-audit требует сети (OSV/PyPI advisory DB). Недоступность фида по умолчанию:
- fail-open: dep-audit не даёт FAIL по причине недоступности фида (иначе — ложные
откаты
→ development→ петля при сетевых проблемах прод-инстанса, прецедент ORCH-061); - громко: в артефакте
deps_audit_degraded: true, логlogger.warning, Telegram-алерт. - Секреты не деградируют: gitleaks offline → гарантия BR-2 безусловна даже при отсутствии сети. Деградирует ТОЛЬКО dep-audit.
- Конфигурируемо: флаг
security_dep_audit_fail_closed(дефолтfalse) позволяет Owner'у переключить на fail-closed (недоступность фида → FAIL) без редеплоя кода.
Это разделяет две гарантии: «нет секрета в прод» — безусловная; «нет известной CVE» — best-effort при доступности фида. Закреплено в acceptance (AC-7).
Р-4. Пороги классификации (A4, BR-10)
- Секреты: любой подтверждённый (не из аллоулиста) секрет → вклад в FAIL (всегда
блок; флаг
security_secrets_block, дефолтtrue). - Зависимости: severity ≥
security_dep_block_severity(дефолтHIGH) → вклад в FAIL (deps_blocking); ниже порога (MEDIUM/LOW) → warning (deps_warning, не блокирует, фиксируется в теле). - Severity = UNKNOWN (OSV/advisory без CVSS — частый случай pip-audit): трактуется как ниже порога → warning, никогда не авто-блок (анти-петля). Логируется.
Р-5. Артефакт и вердикт (FR-3, BR-6, канон проекта)
- Новый артефакт
17-security-report.md(следующий свободный номер; финализировано). - YAML-frontmatter:
Тело — человекочитаемый список находок (секреты: файл/правило/маскированное совпадение; CVE: пакет/версия/идентификатор/severity).
--- security_status: PASS # PASS | FAIL secrets_found: 0 deps_blocking: 0 deps_warning: 2 deps_audit_degraded: false --- - Единый источник истины: гейт вычисляет находки → пишет артефакт → читает вердикт
обратно через
parse_security_status(content)(frontmatter-парсер по образцу_parse_deploy_status/_parse_staging_status) → возвращает этот вердикт. Так возвращаемый(bool, reason)гарантированно == frontmatter артефакта (канон «машинный вердикт — только из YAML-frontmatter, никогда из прозы», AC-8). Negative-токен (FAIL) авторитетен. - Битый/отсутствующий frontmatter / нет поля
security_status→(False, reason)— fail-closed на чтении вердикта (AC-9).
Р-6. Поведение красного гейта (FR-4, BR-5)
security_status: FAIL → врезка _handle_security_gate (по образцу
_handle_image_freshness, но БЕЗ работы с lease — гейт до его захвата):
update_task_stage(development)+enqueue_job("developer", …);- retry-счётчик — существующий
_developer_retry_count(общий с merge/freshness; без новой колонки, TRZ §6); capMAX_DEVELOPER_RETRIES = 3→ при исчерпанииset_issue_blocked+ Telegram; task_descнесёт дословную причину (какие секреты/файлы, какие пакеты/CVE/severity) по образцу ORCH-046 — не только ссылку на артефакт (AC-12);notify_qg_failure+ Plane-коммент (наблюдаемость BR-11).
PASS → return False из обработчика → advance_stage идёт к merge-gate (тишина, без шума).
Р-7. Условный раскат и устойчивость (FR-5/FR-6)
check_security_gate(repo, work_item_id, branch)вQG_CHECKS; обёртка делегирует вsrc/security_gate.py(ленивый импорт во избежание цикла — по образцу_check_staging_image_fresh).- Условность:
security_gate_enabled=False→(True, "security-gate disabled");security_gate_repos(CSV) пусто → реально толькоis_self_hosting_repo→ прочие репо(True, "security-gate N/A for <repo>")(AC-14/AC-15). - never-raise (двойной guard как
check_branch_mergeable): любая ошибка (нет бинаря, таймаут, исключение) →(False, reason), исключение не уходит вadvance_stage(AC-16). - Таймаут сканирования
security_scan_timeout_s(дефолт 300) на каждый внешний вызов (subprocess … timeout=) — превышение → детерминированный degrade-вердикт (AC-17).
Р-8. Self-hosting safety (инвариант TRZ §7.5, AC-19)
Гейт только читает/сканирует (git, gitleaks, pip-audit, запись артефакта). Не вызывает деплой-хук, не рестартит и не трогает прод-контейнер (8500/8501).
Точки касания (для developer; reviewer проверяет полноту — AC-20)
| Модуль | Изменение |
|---|---|
src/security_gate.py (новый leaf) |
security_gate_applies, scan_secrets, audit_dependencies, classify_severity, compute_verdict, write_security_report, parse_security_status, check_security_gate. never-raise, fail-closed на чтении вердикта. По образцу image_freshness.py. |
src/qg/checks.py |
check_security_gate (тонкая обёртка, ленивый импорт) + регистрация в QG_CHECKS. |
src/stage_engine.py |
_handle_security_gate(...) + врезка ПЕРВОЙ в блоке current_stage == "deploy-staging" (до _handle_merge_gate). FAIL → откат на development. never-raise. STAGE_TRANSITIONS НЕ меняется. |
src/config.py |
security_gate_enabled (True), security_gate_repos (""), security_dep_block_severity ("HIGH"), security_scan_timeout_s (300), security_dep_audit_fail_closed (False), security_secrets_block (True) — с docstring по образцу ORCH-043/058. |
Dockerfile |
Установка gitleaks (release-бинарь). |
requirements.txt |
pip-audit. |
.gitleaks.toml (новый, корень репо) |
Конфиг правил + аллоулист (.env.example-плейсхолдеры, тест-фикстуры) — BR-13. |
.openclaw/agents/developer.md |
(Опц.) краткая инструкция про устранение security-находок при заворотах. |
tests/ |
test_security_gate.py, test_qg_security.py, test_stage_engine_security_gate.py (см. 04-test-plan.yaml). |
| Документация | CLAUDE.md (артефакт 17-…), docs/architecture/README.md (таблица гейтов + реестр QG + раздел), CHANGELOG.md, .env.example (ORCH_SECURITY_*), global adr-0012. |
Альтернативы (отклонены)
- Вариант R (review-стадия): раньше/дешевле, но diff может разойтись с тем, что
вольётся в
main; merge-edge уже закрывает «последнюю страховку». - Вариант C (CI-job через
check_ci_green): пороги/severity/аллоулист/артефакт плохо выражаются статусом коммита; коуплинг с CI-раннером. → точка расширения BR-14. - fail-closed dep-audit по умолчанию: ложные откаты при сетевых сбоях → петля. → только опционально через флаг.
- Аудит после rebase (как анкер image-freshness): обвиняет задачу в CVE из
main→ петля. → скан ветки ДО merge-gate. - Новая стадия
security: «пустая» стадия без агента не имеет триггера (как отклонено в ORCH-043). → под-гейт ребра. - Новая колонка retry в БД: не нужна — переиспользуем
_developer_retry_count.
Последствия
Плюсы. Структурно невозможно тихо влить секрет (безусловно) или известную CVE
(best-effort) в main/прод автономной системы. Самоприменение CLAUDE.md §8. Минимальный
blast-radius: STAGE_TRANSITIONS/схема БД не меняются, переиспользован готовый паттерн.
Минусы / плата. Ещё один «скрытый» под-гейт ребра (нет в STAGE_TRANSITIONS).
Добавлены внешние инструменты (gitleaks-бинарь в образ, pip-audit в зависимости). Время
сканирования добавляется к каждому прогону (ограничено таймаутом). Dep-audit best-effort
при сетевых сбоях (осознанный компромисс против петли). v1 — Python-only (A3); мульти-стек
и SAST — follow-up WI (BR-14).
Раскат. Сквозное изменение конвейера (новый QG + новый edge-под-гейт) → лейбл
arch:major-change. Прод-деплой ORCH-022 — строго через staging-гейт (8501), без рестарта
прод-контейнера в рамках задачи (self-hosting safety).
Связи
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
условность/never-raise/fail-closed), adr-0003 (is_self_hosting_repo — образец условности),
adr-0009/ORCH-061 (анти-петля ложных FAIL), ORCH-046 (дословный reason в task_desc),
ORCH-9/15 (мульти-стек — будущая зависимость).