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

ADR 0172 — Релиз сканирует Trivy то, что публикует, по digest, до подписи

Статус​

Accepted. Решено сопровождающим 2026-09-27 (AT-179): сканировать и отклонять. Ссылается на ADR 0119 (образы, публикуемые по digest), ADR 0149 (keyless-подпись cosign по digest) и ADR 0162 (пятый образ). Не редактирует ни один из них.

Контекст​

Trivy существовал только в job images в ci.yml, который сканирует ЛОКАЛЬНУЮ сборку PR (brabo-api:prod в демоне runner). release.yml собирает заново, отправляет в GHCR, регистрирует digests в .release/images.json и подписывает — без какого-либо сканирования. Расследование AT-110 измерило, почему это не тот же артефакт: кэш type=gha, который записывает PR, не читается из тега (из 663 кэшей buildkit репозитория все были в refs/pull/N/merge), так что тег собирается ХОЛОДНЫМ, а apk upgrade финальной стадии выполняется снова и может принести другой набор пакетов. AT-110 закрыла сторону PR (no-cache-filter = ["runtime"] в bake), но образ, который скачивает установщик, оставался единственным, который никто не сканировал.

Решение​

  1. Где. В job release в release.yml, после Registrar as imagens publicadas и до Instalar o cosign/Assinar as imagens publicadas. Порядок и есть механизм: отклонённый образ не получает подписи, а без подписи install.sh его не устанавливает.
  2. Над чем. Над DIGEST каждого образа, зарегистрированного в .release/images.json (repositorio@sha256:…), прочитанного из registry (--image-src remote), — никогда по тегу, по той же причине, что и подпись: тег — подвижный указатель, а digest — то, что было опубликовано. Список берётся из scripts/ci/trivy-do-release.ts referencias, который отклоняет пустой манифест или некорректный digest.
  3. Мерило отклоняет. HIGH или CRITICAL с доступным исправлением проваливает job. Флаги — те же, что в ci.yml, байт в байт: --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore.yaml --skip-db-update --exit-code 1. Решает код выхода Trivy, никогда второе мерило на TypeScript — оно разошлось бы с ним при первом новом Status. Все образы сканируются до отклонения, чтобы журнал назвал, КАКИЕ провалились.
  4. То, что не имеет исправления, сообщается. Второе сканирование каждого образа, без --ignore-unfixed и с --exit-code 0, генерирует JSON; scripts/ci/trivy-do-release.ts resumo собирает markdown (два числа на образ и список того, что не имеет исправления), который идёт в сводку job и прикрепляется к Release как trivy-sem-correcao.md. Этот шаг выполняется ДО ворот, чтобы сводка существовала и тогда, когда ворота отклоняют.
  5. Без нового allowlist. Единственный файл исключений — .trivyignore.yaml, который уже использует ci.yml, — курируемый, с тремя условиями наверху и expired_at в каждой записи. Неиспользование его заставило бы релиз применять мерило СТРОЖЕ, чем у PR, к тем же исключениям, которые сопровождающий уже принял: измерено, engine из v6.1.0 имел бы 56 исправимых находок, все в бинарниках сторонних сканеров, которые документирует этот файл. Spec отклоняет второй --ignorefile, --skip-files, --skip-dirs, --vex, --ignore-policy или --ignore-status.
  6. Та же версия, объявленная дважды, под охраной. TRIVY_VERSION и TRIVY_SHA256 входят в env: release.yml со значением из ci.yml. Одно место не подошло: env: workflow не импортируется из другого файла, а составная action для двух переменных была бы третьим местом для сопровождения. Бинарник скачивается через curl и проходит sha256sum -c перед распаковкой, как в ci.yml. Без новой action (ни один uses: не вошёл).
  7. Доказательство. release.yml выполняется только на финальном теге, поэтому ни один PR его не исполняет — ситуация install-e2e.yml. scripts/ci/trivy-do-release.spec.ts охраняет в каждом PR: ворота после регистрации и до cosign; ни одного continue-on-error/always() на пути; флаги мерила, равные флагам ci.yml; отчёт без --ignore-unfixed и с --exit-code 0; версия и хеш равны; curl → sha256sum -c → tar; ассет в gh release create. Доказано мутацией: удаление --ignore-unfixed из ворот или перенос cosign-installer перед ними делает spec красным.

Последствия​

  • Отклонение оставляет опубликованный образ без подписи. Push происходит до сканирования (именно он создаёт digest), поэтому отклонённый тег оставляет :X.Y.Z и :<sha> в GHCR, без подписи, без Release и без прикреплённого images.json. Это правильное состояние — установщик отказывается от образа без подписи, — но это публичный остаток. Переделка — исправить базу и выпустить новый тег; повторная публикация через workflow_dispatch того же тега пересобирает и пересканирует.
  • Следующий тег может родиться красным из-за CVE, опубликованной после PR. Сканирование тега использует базу уязвимостей на день тега, а не PR. В этом суть: то, что доходит до установщика, судится по тому, что известно в день, когда оно доходит. Измерено 2026-09-27 по digests v6.1.0: api и web отклонялись бы сегодня из-за CVE-2026-45447 (HIGH, libcrypto3/libssl3 3.3.7-r0 → 3.3.7-r1), CVE, опубликованной после того тега; engine и backup проходят; ничего HIGH/CRITICAL без исправления. Новый тег собирается холодным и выполняет apk upgrade, так что должен принести 3.3.7-r1 — что здесь не доказано, потому что потребовало бы выпустить тег.
  • Стоимость по времени. Измерено локально на четырёх digests v6.1.0: 13 с на загрузку базы и ~17 с на восемь сканирований (ворота и отчёт), по сети. На runner реальное число появится только с первым тегом; job остаётся с timeout-minutes: 30, против 4–11 мин трёх последних релизов.
  • .trivyignore.yaml начинает действовать и для публикуемого. Запись, срок которой истекает (expired_at: 2026-10-31 в текущих), отклоняет PR И следующий тег. Дисциплина пересмотра списка не меняется; меняется то, что забывчивость теперь блокирует публикацию, а не только merge.
  • Объявленное дублирование. Версия, хеш и флаги существуют в двух workflows. Spec отклоняет расхождение; кто обновляет Trivy, обновляет в обоих в одном PR.