{ "index": 165, "slug": "editorial-2023-06-practice-secrets-supply-chain", "title": "Секрет в CI и digest образа: как связать звенья цепочки поставки", "excerpt": "Секрет не должен становиться частью образа, а deploy должен ссылаться на тот же digest, который прошёл проверки. Разбираем границы, evidence и отрицательный путь.", "contentHtml": "

В релизе есть запись о deploy, но никто не может быстро ответить на четыре вопроса: из какой ревизии собрали образ, какой процесс его собрал, использовал ли build секрет и какой digest действительно запустили. Симптом часто выглядит безобидно: pipeline зелёный, сервис работает, а расследование останавливается на фразе «образ собрал CI». Цена ошибки появляется позже. Если токен попал в слой образа или deploy взял соседний тег, команда не может надёжно определить затронутый артефакт, отозвать доступ и объяснить происхождение выпуска.

\n

Тезис простой: цепочку поставки нужно проверять как связь фактов, а не как набор названий инструментов. Ревизия исходников, идентичность сборщика, граница секрета, digest образа, утверждение о происхождении и вход deploy должны иметь общий ключ и понятного владельца проверки. Декларация «образ подписан» не заменяет сопоставление subject с digest. Наличие переменной `TOKEN` в CI не доказывает, что её значение не попало в лог или слой образа.

\n

Механизм: секрет проходит этап, но не должен проходить в результат

\n

Секрет нужен build только на коротком шаге: например, чтобы скачать закрытую зависимость. Процесс должен получить ссылку на секрет, использовать её внутри команды и не записать значение в переменную окружения, слой, кэш или stdout. Образ после этого содержит приложение и публичные настройки, но не credential. В Docker BuildKit для такой границы используют secret mount. Build argument и обычная переменная окружения для этого не подходят: они могут сохраниться в истории сборки или финальном образе.

\n
# syntax=docker/dockerfile:1\nFROM node:22-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\n\nRUN --mount=type=secret,id=npmrc,target=/root/.npmrc \\\n    npm ci --ignore-scripts\n\nCOPY . .\nRUN npm run build\n\nFROM nginx:alpine\nCOPY --from=build /app/dist /usr/share/nginx/html
\n

Команда запуска должна передать секрет отдельно от контекста сборки. В учебном примере ниже имя файла условное. Не подставляйте настоящий токен в статью, shell history или CI log.

\n
DOCKER_BUILDKIT=1 docker build \\\n  --secret id=npmrc,src=/path/to/temporary/npmrc \\\n  --tag example/app:build-123 .\n\n# После сборки получить digest из registry и сохранить его\n# как значение, с которым сравниваются attestation и deploy.
\n

Этот код показывает границу передачи. Он не доказывает, что конкретный runner настроен безопасно, что registry доверенный и что deploy использовал правильный digest. Эти утверждения требуют наблюдаемых записей из вашей среды.

\n

Один пример связи фактов

\n

Представьте выпуск `release-123`. Исходная ревизия — `git:abc123`. Сборщик сообщает identity `ci/build-prod`. Registry возвращает digest `sha256:7f...`. Attestation имеет subject с тем же digest и ссылается на `git:abc123`. Deploy получает не тег `build-123`, а полный digest. Тогда расследование может пройти по одной цепочке. Если хотя бы одно звено хранит только свободный текст, связь становится гипотезой.

\n

Тег удобен для человека, но изменяем. Digest адресует конкретный результат. Поэтому тег можно показывать в интерфейсе, а проверяемым входом выкладки считать digest. Если attestation относится к `sha256:91...`, а deploy запускает `sha256:7f...`, выпуск нужно остановить. Нельзя исправить несовпадение новым комментарием в release.

\n
\"Границы
Карта границ доверия. Она помогает назвать проверяемые факты, но не является журналом реального CI и не подтверждает подпись образа.
\n

Симптомы и действия

\n
Как перейти от сигнала к проверке
СимптомПричинаПроверкаДействие
В логах виден фрагмент токенаСекрет попал в stdout, debug или командную строкуПроверить логи шага и маскирование, затем поискать значение в слоях и артефактахОтозвать credential, очистить путь вывода и повторить сборку без утечки
В Dockerfile есть `ARG TOKEN`Секрет передаётся как параметр и может остаться в историиПроверить history и metadata образаПерейти на secret mount и выпустить новый digest
Attestation и deploy ссылаются на разные digestВыкладка использует изменяемый тег или другой outputСравнить точные subject и deploy inputОстановить выпуск, выбрать проверенный digest, выяснить источник расхождения
Есть подпись, но нет записи о builderПроверяют целостность statement, но не происхождение сборкиПроверить identity подписанта и поля provenanceРазделить проверку подписи, builder и исходной ревизии
После deploy нельзя найти исходную ревизиюRelease хранит только номер задачи или короткий тегСверить metadata образа, CI run и commitСделать revision обязательным полем evidence
\n

Порядок действий

\n
  1. Выберите один выпуск и зафиксируйте его точный deploy input. Если система принимает тег, получите digest, который фактически использовал runtime.
  2. Найдите исходную ревизию, из которой собрали этот digest. Не подменяйте её веткой: ветка меняется, commit остаётся идентификатором состояния.
  3. Определите identity builder и сохраните ссылку на конкретный запуск. Запись «собрано CI» недостаточна.
  4. Проверьте границу секрета: где его запросили, какой шаг получил доступ и какие файлы, слои, логи и кэши могли его сохранить.
  5. Сопоставьте subject attestation с digest образа. Затем отдельно проверьте подпись, доверенную identity и ожидаемую ревизию.
  6. Сравните этот же digest с входом deploy. При несовпадении не продолжайте выпуск и не заменяйте digest повторным тегированием.
  7. Зафиксируйте результат и владельца следующей проверки. Для каждого неизвестного факта оставьте статус «не подтверждено».
\n

Отрицательный путь: что делать при разрыве

\n

Наиболее опасная ветка начинается с частичного успеха. Образ собрался, тесты прошли, а subject attestation не совпал с digest deploy. В этот момент нельзя считать выпуск безопасным из-за зелёного pipeline. Остановите продвижение, сохраните безопасные метаданные, определите последний проверенный digest и выясните, где возникло расхождение: в registry, в выборе тега, в подготовке attestation или в конфигурации deploy.

\n

Если секрет уже попал в лог или образ, удаление строки не возвращает безопасность. Отзовите и замените credential по правилам вашей платформы. Удалите доступный артефакт, проверьте кэши и логи, а затем соберите новый образ с другим digest. Не утверждайте, что утечки не было, если проверка охватила только git и не охватила registry или CI.

\n

Ограничения

\n

Пример с Docker — учебный. В нём нет настоящего секрета, registry, CI run, подписи или deploy; плейсхолдер `/path/to/temporary/npmrc` нельзя использовать как production-рецепт. Secret mount снижает риск записи значения в финальный слой, но не защищает от команды, которая сама печатает секрет, сохраняет его в собранный файл или отправляет его в сеть. Secret scanning помогает обнаружить известные шаблоны, но не доказывает отсутствие всех credential.

\n

Provenance описывает заявленные входы и исполнителя. Оно не делает builder доверенным само по себе. Подпись подтверждает связь statement с ключом или доверенной identity, но не превращает любое утверждение в факт. Полная проверка зависит от политики организации, runner, registry, формата attestation и правил deploy. Поэтому статья не заявляет production-результатов и не заменяет проверку конкретной платформы.

\n

Критерий готовности

\n

Материал можно считать применённым к одному выпуску, когда команда без устных пояснений показывает: commit исходников, identity builder, границу доступа к секрету, digest образа, проверенное соответствие subject этому digest и тот же digest на входе deploy. Для отрицательного пути есть запись о том, что происходит при несовпадении. Если хотя бы одного поля нет или его нельзя проверить по первичному источнику, выпуск не помечают как подтверждённый.

\n

Проверяемые источники

\n" }