Цепочка поставок CI
Каждый job этого репозитория запускает сторонний код на машине, где есть
checkout исходного кода, а в workflow релиза — ещё и учётные данные
registry GHCR, registry npm и самих git-ссылок репозитория. Этот код
приходит двумя путями: GitHub Actions (uses:) и бинарники,
скачанные через curl (сканеры). Ни то, ни другое не пишется здесь, и ни
то, ни другое не проходит ревью в PR.
Эта страница посвящена той части, которую мы контролируем. Она существует потому, что механизм жил только в комментариях workflow — там, где правило можно прочитать, но нельзя проверить, и где две половины разошлись именно по этой причине.
Угроза без обиняков
Тег — это указатель. actions/checkout@v4 — не версия, а имя, которое
владелец action может в любой момент удалить и создать заново, указав на
другой коммит, без какого-либо сигнала в этом репозитории. Тот, кто
перемещает этот тег, запускает свой код на нашем runner'е, в нашем
checkout, с секретами, которые получил этот workflow.
То же самое верно для curl ассета GitHub Release: скомпрометированный
релиз или MITM между runner'ом и CDN подсовывает другой бинарник, и никто
этого не замечает — пока сканер не перестанет запускаться или, что хуже,
пока он «работает».
Ни то, ни другое не является гипотезой именно о нашем репозитории. Это два задокументированных способа компрометации CI в реальном мире, и защита от обоих — одна и та же идея: ссылаться на содержимое, а не на имя.
Два механизма
Бинарники: контрольная сумма после каждой загрузки
За каждым curl ассета релиза в ci.yml и в обоих Dockerfile движка
следует sha256sum -c против хеша, записанного в самом workflow:
GITLEAKS_SHA256: '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb'
Хеши берутся из checksums.txt, публикуемого с каждым релизом, отдельной
загрузкой. Расхождение проваливает job ещё до того, как бинарник будет
хотя бы запущен.
propriedades.yml (плановые доказательства свойств, BRB-009) следует тому
же правилу для трёх бинарников, нужных ему для подъёма кластера, — k3d,
helm и kubectl, у каждого свой хеш в env: workflow. Он устанавливает
их до запуска deploy/k8s/bootstrap.sh, и в этом порядке вся суть:
ensure_k3d самого bootstrap скачивает k3d без контрольной суммы, и
именно наличие бинарника в PATH не даёт ему скачивать. Отдельный шаг
проваливает job, если закреплённые там версии расходятся с теми, что
установил бы bootstrap. Что этот workflow по-прежнему принимает на веру,
заявлено: чарты Helm приходят из своих репозиториев по версии, а не по
digest (deploy/k8s/helm/charts.env), вместе с образами, на которые эти
чарты ссылаются; а k3d скачивает образ узла rancher/k3s и образ своего
балансировщика нагрузки по тегу, зашитому в бинарник k3d, — то есть
они закреплены лишь настолько, насколько их закрепляет контрольная сумма
k3d.
У версий сканеров есть второе ограничение: они должны совпадать с
docker/engine/Dockerfile.prod, потому что тестировать против сканера,
отличного от работающего в production, — ложный зелёный. Комментарий,
обещавший это, был там со времён Фазы 5; шаг, который это проверяет
(Versões dos scanners batem com o Dockerfile.prod do engine, в job
lint), появился только с #408.
Actions: SHA коммита с версией рядом
Каждый uses: закреплён на 40-символьном SHA коммита, а тег, из которого
он взят, сохранён в комментарии в конце строки:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Комментарий обязателен, и не для красоты. Это единственное, что сообщает человеку — и Dependabot, который читает именно этот комментарий, — какой версии соответствует хеш. SHA без версии рядом — это закрепление, которое никто не может проверить и никто не может обновить.
Чтобы разрешить тег в SHA для записи:
gh api repos/actions/checkout/commits/v4 --jq .sha
Почему есть проверка, а не только правило
#408 закрепил actions в ci.yml и только в нём. Остальные пятнадцать
workflow месяцами оставались на изменяемых тегах — включая release.yml
(пушит образы в GHCR), publish-runner.yml (публикует в npm),
tag-release.yml (создаёт теги) и docs-deploy.yml (пушит в
gh-pages). Незакреплённой осталась именно та половина, у которой есть
учётные данные.
Это не небрежность, а предсказуемая форма правила без механизма: новый
workflow пишется копированием соседа, а у соседа был тег. Поэтому теперь у
правила есть механизм — scripts/ci/actions-pinadas.ts, запускаемый в job
lint, который проваливает любой uses:, не являющийся SHA коммита, и
любой SHA без комментария с версией. Это то же рассуждение, что и
в подсчётах RN и ADR: число, поддерживаемое
вручную, устаревает в тот момент, когда кто-то забывает, поэтому артефакт
читается, а не принимается на доверие.
Ссылка на action внутри этого репозитория (./.github/...)
проходит: это наш код, проверяемый PR, который его меняет, без третьей
стороны, способной что-либо переместить.
Браузер Playwright
Браузерный E2E (ADR 0120) скачивает chromium во время CI из CDN Playwright — третья загрузка, которая не является ни action, ни ассетом GitHub Release, поэтому ни один из двух механизмов выше её не покрывает.
Закрепляет её точная версия @playwright/test в
e2e/pnpm-lock.yaml: каждый релиз пакета привязан к сборке браузера, и
playwright install загружает именно её. Значит, закреплением служит
lockfile, а --frozen-lockfile в job превращает его в закрепление, а не в
рекомендацию.
sha256sum -c здесь нет, и это честное описание: инструмент скачивает и
проверяет сам, а воспроизводить его таблицу контрольных сумм вручную
означало бы держать устаревающую копию. Слабее, чем бинарники сканеров, —
сказано прямо, а не подразумевается.
Что job образов хранит в кеше и что кеш не может изменить
Начиная с AT-304 job images в ci.yml хранит между запусками три вещи с
помощью actions/cache (закреплённого по SHA, как и любой action), и
каждая выбрана так, чтобы кеш экономил время, не сдвигая вердикт:
| кеш | ключ | что определяет содержимое |
|---|---|---|
браузер Playwright (~/.cache/ms-playwright) | точная версия @playwright/test + хеш e2e/pnpm-lock.yaml | закрепление lockfile выше — новый Playwright означает новый ключ, никогда не старое попадание |
store pnpm для e2e/ (setup-node, cache-dependency-path: e2e/pnpm-lock.yaml) | хеш e2e/pnpm-lock.yaml | pnpm install --frozen-lockfile, который по-прежнему сверяет целостность каждого tarball с lockfile |
база уязвимостей Trivy (/tmp/trivy-cache/seed/db) | версия Trivy + день UTC, с восстановлением самого свежего предыдущего дня | сам Trivy: --download-db-only по-прежнему запускается, читает NextUpdate самой базы и скачивает заново, когда срок прошёл |
Последняя строка — та, что могла бы ослабить gate, но не ослабляет: кеш
лишь засевает каталог, а свежесть оценивается тем же правилом, которое
Trivy применяет на ноутбуке. Кеш убирает загрузку ~110 МБ из registry при
каждом запуске — она же была источником эпизодического сбоя
TOOMANYREQUESTS. release.yml этот кеш не использует: тег сканируется с
базой, которую он скачивает сам.
Кеш браузера хранит только браузер. Системные библиотеки, которые
ставит --with-deps, уходят в apt runner'а, который никакой кеш каталога
сохранить не может, поэтому тёплый запуск всё равно их устанавливает
(playwright install-deps).
Область видимости определяет GitHub, а не мы: ci.yml запускается только
на pull_request, поэтому эти кеши записываются в области самого PR и
читаются последующими push этого PR (и областью базовой ветки, в которую
этот workflow никогда не пишет). Один PR не может засеять кеш другого PR.
Образы контейнеров: digest с тегом внутри ссылки
Сторонние образы закрепляются по digest, а тег, из которого они взяты,
пишется внутри ссылки, перед digest —
image:tag@sha256:<index>
(ADR 0178):
image: neo4j:5.26-community@sha256:d9cfe82983d27f5a75b3aaae8f316d04f9a698a3b7f6103a508f7caf8362f255
FROM node:26.12.1-alpine3.24@sha256:40cd011b55415bc894e45b02fdd5a5dfde056921b7d23a64c74f80497af48797 AS deps
Когда присутствуют оба, Docker скачивает по digest: тег — информация для читающего строку, а байты определяет digest.
До ADR 0178 тег жил в комментарии (neo4j@sha256:… # 5.26-community,
а в Dockerfile — на строке выше), по образцу actions. Он сменил место из-за
измерения (AT-139, 2026-09-27): Dependabot не читает этот комментарий ни в
каком формате, а закрепление только по digest «обновляется» до digest
latest — что увело бы pgvector, neo4j и node на другой major, при этом
комментарий по-прежнему утверждал бы старый тег, а lint был бы зелёным. С
тегом inline Docker, Dependabot и человек читают один и тот же тег.
imageName у CloudNativePG уже имел такую форму по другой причине: его
webhook отвергает ссылку только с digest, потому что читает версию Postgres
из тега.
Комментарий по-прежнему разрешён, но только если он говорит то же
самое: комментарий, утверждающий тег (один токен с цифрой), отличный от
inline, проваливает проверку — это именно тот остаток, который бросила бы
смена версии. В Dockerfile комментарий в конце строки FROM по-прежнему
проваливает проверку: парсер Docker распознаёт # только в начале
строки, поэтому FROM alpine:3.20@sha256:… # 3.20 — это FROM с тремя
аргументами, и сборка падает с "FROM requires either one or three
arguments". Это было обнаружено правильным образом при ADR 0159 — шаг
bake job images упал при первом push за 27 секунд, — а hadolint
пропустил тот же файл: согласие линтера — не согласие сборки.
Эта страница раньше утверждала обратное — закрепление по тегу было
«осознанной остановкой, а не небрежностью», покупающей повседневную
воспроизводимость «без затрат на сопровождение digest для образов, которые
мы не публикуем». Это рассуждение отменено, и причина — измерение: все
37 сторонних ссылок в репозитории были на тегах, и три места, где они
выполняются, хуже, чем устаревший docker compose pull.
- Строки
FROM— основа образов, которые мы публикуем в GHCR, — их пять начиная с ADR 0162. Перемещённый тег превращается в байты внутри образа, который мы подписываем и отдаём другим людям. docker/docker-compose.install.ymlработает на машине того, кто установил продукт, рядом с его Postgres. В этом же файле наши собственные образы уже приходят по digest, через переменную, которую заполняет установщик, — а сторонние приходили по тегу, рядом с ними.ci.yml,golden-set-rag.ymlиgolden-set-qa.ymlзапускают сторонние образы какservices:job. Это буквально тот runner, ради защиты которого существует правило для actions, достигнутый через другую дверь. Начиная с ADR 0197 они не хранят никаких литералов: см. Workflow читают compose ниже.
Чтобы разрешить тег в digest для записи — digest индекса, чтобы
закрепление продолжало работать на linux/arm64, помимо linux/amd64:
docker manifest inspect neo4j:5.26-community | head -3 # confirms it is an index
docker buildx imagetools inspect neo4j:5.26-community --format '{{.Manifest.Digest}}'
Тег обязателен по той же причине, по которой обязателен комментарий с
версией у actions: sha256:22ec5cd0… никому не говорит, что это Neo4j
5.26. И, как с actions, у правила есть механизм, а не добрая воля, —
scripts/ci/imagens-pinadas.ts, запускаемый в job lint, который
проваливает любой image:/imageName:/FROM, не являющийся digest, любой
digest без тега inline (включая старую форму только с комментарием),
комментарий с тегом, расходящийся с тегом inline, комментарий в конце
FROM и один и тот же тег inline с двумя разными digest в двух файлах. Он
родственник actions-pinadas.ts, а не его расширение: uses: живёт в
YAML workflow с одним синтаксисом, образы живут в compose, манифестах
kustomize и Dockerfile с тремя другими, и функция, отвечающая на оба
вопроса, ответила бы плохо на оба.
Что проверка намеренно не покрывает:
- Образы, которые мы собираем сами (
brabo-api,brabo-engine,brabo-web,brabo-backupиbrabo-broker— последний публикуется начиная с ADR 0162). Нет третьей стороны, которая могла бы что-то переместить, аbrabo-api:prod— локальный тег, digest которого до сборки не существует. Там, где они проходят через registry, они уже идут по digest, через механизм, который ими владеет:.release/images.json, записываемыйrelease.ymlи применяемыйmake imagens-do-release(ADR 0119). Overlay репозитория хранит маркер, а не замороженный релиз — требование литерального digest там противоречило бы этому механизму, вместо того чтобы его усиливать. - Интерполированные ссылки (
${BRABO_API_IMAGE:?…}) — тот же случай, увиденный с другой стороны. - Стадии multi-stage сборки (
FROM deps AS build,FROM scratch), которые не являются образами registry.
Список исключений задаётся по имени и закрыт при сбое: новый сторонний
образ никогда не совпадёт с brabo-, поэтому он рождается под правилом.
Workflow читают compose
До ADR 0197
pgvector и ollama жили в двух местах: в compose и литералами в
services: у ci.yml и двух workflow golden-set. Ни одна экосистема
Dependabot не читает services: workflow, поэтому каждый PR бота,
поднимающий один из них в compose, рождался бы красным из-за правила
«один тег, два digest», а шаг, который выровнял бы workflow в PR бота, не
может запушить с GITHUB_TOKEN, у которого никогда нет права менять
.github/workflows/.
Литерал ушёл. Переиспользуемый workflow
.github/workflows/imagens-do-compose.yml запускает
scripts/ci/imagens-do-compose.ts, который читает image: каждого сервиса
из docker/docker-compose.yml (и отвергает тот, что не имеет вида
image:tag@sha256:<index>), а вызывающие пишут
image: ${{ needs.imagens.outputs.pgvector }}. Несуществующий дубликат не
может разойтись: PR бота трогает только compose, и CI того же PR уже
работает против нового образа. То, что golden-set-rag.yml обещал в
комментарии, — тот же Ollama, что и в dev, потому что нижняя граница
golden-set привязана к модели, а не к окружению, — теперь верно по
построению.
imagens-pinadas.ts выучил новую форму и закрывает дверь, которую она
открывает. В workflow он проваливает любой сторонний литерал
(закреплённый или нет); любое выражение, не являющееся в точности
${{ needs.<job>.outputs.<name> }}, — литерал внутри выражения, значение
по умолчанию через ||, env., vars., format() — всё это места, где
могла бы спрятаться изменяемая ссылка, а старый шаблон даже не совпадал со
строкой с пробелами; и needs.<job>, чей job не вызывает переиспользуемый
workflow, или выход, который он не объявляет. Каждое правило доказано
мутацией в своём spec.
Цена реальна, и часть её теперь платит Dependabot. Образ, закреплённый
по digest, не получает обновлений безопасности, пока кто-то не сменит
digest, — тот же долг, что несут SHA у actions. .github/dependabot.yml
включает github-actions по этой причине и, начиная с ADR 0197,
docker-compose (/docker) и docker (/docker/* и
/deploy/k8s/**), каждый сгруппирован в еженедельный PR в dev.
Временный ignore MAJOR для node (AT-360) ушёл с переходом на Node 26
(решение владельца, 2026-10-02): обе экосистемы образов теперь предлагают
major-версии node, как любое другое обновление.
По-прежнему вручную, и это заявлено в ADR 0197: neo4j и ollama живут
также в deploy/k8s/base/, две экосистемы никогда не делят один PR, а
перевыпуск digest с тем же тегом приводит к падению обоих PR, пока человек
их не объединит; imageName у CloudNativePG и IMAGEM_DO_GOLDEN_SET_QA не
читаются ни одной экосистемой. Они следуют
процедуре runbook.
Digest гарантирует неизменяемость, а не доступность. Закрепление
защищает от того, что владелец тега его переместит; оно не защищает от
того, что издатель удалит репозиторий. Так случилось с MinIO: S3-сервер
локального overlay был закреплён по digest индекса в
quay.io/minio/minio, MinIO перестал публиковать community-образ, и тот
же digest стал отвечать 401 (Docker Hub: 404). Bootstrap падал на
rollout status, и ни одно доказательство свойств не запускалось, пока
сервер не заменили на SeaweedFS
(ADR 0169).
Симптом, если это повторится с любым закреплённым образом, —
ImagePullBackOff на digest, который не менялся; исправление — новый
образ от издателя, который его ещё раздаёт, но никогда не снятие
закрепления.
Кеш сборки не может скрыть устаревший пакет
Финальная стадия (runtime) каждого Dockerfile.prod выполняет
apk upgrade. BuildKit строит ключ слоя по инструкции и родительскому
слою, а никогда не по времени, поэтому при включённом cache-from сборка
PR переиспользовала слой apk upgrade, замороженный при первой сборке
этого PR (измерено: CACHED в 5 из 5 образов), и Trivy сканировал старый
openssl, которого тег, собранный с нуля, не содержал бы. Поэтому
docker-bake.hcl задаёт no-cache-filter = ["runtime"] в общей базовой
цели: финальная стадия и всё, что после неё, пересобираются каждый раз,
стадии сборки сохраняют свой кеш (именно там время), а сканирование видит
то, что опубликовал бы тег. Это не allowlist Trivy. Стоимость — около
7–9 с на образ, параллельно.
Это исправило то, что сканирует PR. Это не могло исправить то, что публикует тег: тег собирается с нуля, в другом запуске, и до ADR 0172 его ничто не сканировало.
Релиз сканирует то, что публикует, до подписи
release.yml запускает Trivy для каждого образа по digest, прямо из
registry (--image-src remote), после того как bake запушил и записал
.release/images.json, и до cosign sign. Порядок и есть механизм:
образ, не прошедший проверку, никогда не подписывается, а install.sh не
устанавливает неподписанный образ. Push к этому моменту уже произошёл
(именно он создаёт digest), поэтому проваленный тег оставляет свои теги в
GHCR без подписи и без Release — правильное состояние, ведь ничто их не
устанавливает.
Gate — то же правило, что и job в ci.yml, флаг за флагом:
--scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 и
тот же .trivyignore.yaml, где у каждой записи есть expired_at. HIGH
или CRITICAL с доступным исправлением проваливает релиз. То, у чего
исправления нет, сообщается без блокировки: второе сканирование без
--ignore-unfixed и с --exit-code 0 питает
scripts/ci/trivy-do-release.ts, который пишет сводку job и ассет
Release trivy-sem-correcao.md. Этот скрипт никогда не выносит вердикт —
его выносит код выхода Trivy, — потому что второе правило для «есть
исправление» разошлось бы с правилом самого Trivy при первом же
добавлении им нового статуса. Нового allowlist нет: шаг не принимает
файл игнорирования, кроме того, что уже использует PR.
Бинарник — тот же v0.70.0 с тем же sha256, что и в ci.yml,
объявленный дважды, потому что env: одного workflow нельзя
импортировать. За дублированием следят: scripts/ci/trivy-do-release.spec.ts
проваливается, когда версия, хеш или флаги gate расходятся между двумя
workflow, когда gate переносится после подписи или когда появляется
второй файл игнорирования. Как и install-e2e.yml, релиз запускается
только на финальном теге, поэтому этот spec — то доказательство, которое
может дать PR.
Измерено 2026-09-27 против digest последнего релиза (v6.1.0, четыре
образа, опубликован 2026-09-14), с теми же флагами: api и web
не прошли бы — CVE-2026-45447 (HIGH, libcrypto3/libssl3
3.3.7-r0, исправлено в 3.3.7-r1), CVE, опубликованная после того тега,
— engine и backup проходят, и ничего HIGH/CRITICAL без исправления не
появляется после применения .trivyignore.yaml. Без него engine нёс бы
56 исправимых находок в сторонних бинарниках сканеров, которые этот файл
документирует. Четыре сканирования плюс загрузка базы заняли около 30 с.
Зависимость, закреплённая на git-коммите: bandit, временно
Каждая зависимость движка приходит из hex.pm, с одним ВРЕМЕННЫМ
исключением. Advisory EEF-CVE-2026-105659 (GHSA-69m6-wcv4-rwcf, HIGH,
HTTP/2 в Bandit) затрагивает любой Bandit >= 1.4.0, включая 1.12.5,
последний релиз в hex, и mix hex.audit в CI проваливает из-за него каждый
PR. Исправление существует только как коммит
d88b21953c65061f21136ea76a89a4f95a2ac7a1 в mtrudel/bandit (смержен
2026-10-12), без релиза. С 2026-10-14 apps/engine/mix.exs указывает
bandit на этот коммит, по полному SHA и никогда по ветке, с
override: true, потому что phoenix и opentelemetry_bandit требуют
пакет из hex (измерено: без этого mix deps.get останавливается с
"Dependencies have diverged"). SHA играет ту же роль, что контрольная сумма
для пакета из hex: lock его записывает, и никто не может его переместить.
Оба Dockerfile движка уже устанавливают git на стадии сборки.
Путь назад: вернуться к {:bandit, "~> 1.12"} из hex, как только будет
опубликован релиз, содержащий этот коммит, и удалить этот раздел вместе с
этим.
Что до сих пор принимается на веру
Заявлено, не исправлено:
-
Без Dependabot.Половина закрыта, а другую половину закрывать не нам. Утверждение также перестало быть верным: обновления безопасности включаются в интерфейсе репозитория, ни в каком файле, и выполнялись ежедневно противnpm_and_yarn in /., /website— и падали каждый день сDependabot::DependabotError. Измеренная причина: пакеты, за которыми он гоняется (multer,esbuildи список рядом с ними), — те, что этот репозиторий исправляет через overrides вpnpm-workspace.yaml, механизм, с которым Dependabot работать не умеет. Alert, за которым он гоняется, уже закрыт; остаётся ошибка..github/dependabot.ymlтеперь существует и делает две вещи, которые может сделать файл: включаетgithub-actions— долг, названный прямо здесь, поскольку SHA перемещаются вручную, а закрепление, которое никто не обновляет, — это закрепление, стареющее до заведомо уязвимой версии, — и объявляет два дерева npm сopen-pull-requests-limit: 0, что отключает рутинные обновления версий, не затрагивая обновления безопасности. Отключение обновлений безопасности — переключатель интерфейса, доступный только владельцу репозитория; пока кто-то не решит, этот job продолжает падать на alert'ах, которые overrides уже закрыли.Каждый PR Dependabot входит через
dev(target-branch: devво всех пяти записях — включая обе экосистемы образов, начиная с ADR 0197). Обновления безопасности игнорируют этот ключ и открываются против ветки по умолчанию, поэтомуdependabot-para-dev.ymlзакрывает те, чтоdevуже исправляет, и перенаправляет остальные — см. Dependabot enters through dev вbranching-policy.md. -
Зависимости npm/pnpm не аттестуются. Lockfile закрепляет версии и хеши целостности, и это реально, но проверки происхождения (
npm audit signaturesили аналог) нет ни в одном job. -
Нет подписи и аттестации наших собственных артефактов.Закрыто ADR 0149 (BRB-005).release.ymlподписывает публикуемые образы по digest с помощью keylesscosign— OIDC-идентичность workflow, без ключа на хранении где-либо, — аbuild-runner-binaries.ymlполучил консолидирующий job, публикующий один подписанныйchecksums.txt, покрывающий бинарники runner (четыре цели начиная с ADR 0174, убравшегоdarwin-x64), вместо подписи для каждого бинарника. Оба workflow проверяют то, что только что подписали, в том же запуске: подпись, которую никто не пытается проверить, — просто ещё один файл в релизе, а сбой иначе проявился бы на машине устанавливающего — худшем месте для его обнаружения.Начиная с ADR 0160 этот манифест покрывает больше, чем бинарники:
install.shи четыре файла, нужные ему для подъёма установки, — compose установки и три файла, которые он использует по относительному пути, публикуемые как ассетыbrabo-install-*из checkout тега, — являются строками того жеchecksums.txt, и job выполняет над нимsha256sum -c --strictперед тем, как что-либо прикрепить. Установщик сверяет каждый из четырёх с этим манифестом, прежде чем что-либо спрашивать или записывать (RN-570). До этого compose был единственным, что использовал установщик и что ничто не проверяло — он даже не скачивался. Список живёт вscripts/ci/assets-do-instalador.ts, и его spec проваливается, когда копия в самом установщике расходится или когда compose получает относительный bind-mount, которого в нём нет.Образ broker — пятый, и он важнее всех (ADR 0162). До этого его никто не собирал:
Dockerfile.prodсуществовал и проходилhadolint, но ни одна цель bake его не собирала, поэтому он никогда не сканировался и не публиковался, и установке нечем было предложить этот сервис. Теперь он проходит каждый gate, который проходят остальные четыре, —ci.ymlсобирает его в каждом PR, отвергает запуск от root, запускает на нём Trivy и поднимает его здоровым с rootfs только для чтения и без сети;release.ymlпубликует, записывает, подписывает и проверяет его по digest без лишней строки, потому что оба цикла читают.release/images.json. Причина, по которой ему это нужно больше, чем любому другому: в установке, давшей согласие, это единственный сервис, получающий socket Docker хоста, поэтому уязвимость в нём — путь ко всей машине. Его публикация делает его публичной целью для изучения; эта цена заявлена в ADR, а не скрыта. Overlay Kubernetes о нём не знает (argumentosDeSetImageвыдаёт только четыре образа, которые объявляет база kustomize), — Deployment для broker нет, по решению.Чего это не покрывает, и это отдельный пункт: подписи кода бинарников runner для операционной системы (нотаризация macOS, Authenticode Windows), которая требует платной идентичности подписи и остаётся в backlog.
-
Сторонние образы закреплены по тегу, а не по digest.Закрыто (выше): все 33 сторонние ссылки — compose, манифесты kustomize и строкиFROMв Dockerfile — закреплены по digest с тегом внутри ссылки (ADR 0178),services:у workflow читают свои из compose (ADR 0197), аscripts/ci/imagens-pinadas.tsпровалит joblintпри следующей регрессии. Что не закрыто, и это цена закрепления, а не остаток: digest не получает обновлений безопасности, пока кто-то его не поднимет. Экосистемыdocker/docker-composeв Dependabot предлагают эти поднятия начиная с ADR 0197; то, до чего они не дотягиваются, перечислено там. -
Разрешения самих workflow здесь не покрываются; это блок
permissions:каждого workflow, и это отдельный аудит. -
Повторный таймаут
pnpm audit— ПРИНЯТЫЙ РИСК, по решению. Когда endpoint advisories npm перестаёт отвечать три раза подряд, job остаётся зелёным с громким предупреждением, а не красным — см. ADR 0140. Чего это стоит, говорится прямо: в таких запусках дерево зависимостей не проверяется, и запуск говорит об этом, а не намекает на чистый результат. Чего это не касается: реально сообщённая уязвимость по-прежнему проваливает с первой попытки, без retry, а нераспознанный сбой тоже проваливает (закрыто при сбое). Альтернатива — оставаться красным, пока третья сторона недоступна, — была измерена в день, когда это случилось (четыре PR заблокированы одним и тем же 503), и отвергнута, потому что gate, который кричит «волки», учит людей перезапускать до зелёного, а эта привычка не отличает настоящий красный от ложного.