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