{ "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Секрет нужен 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.
\nDOCKER_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Представьте выпуск `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| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В логах виден фрагмент токена | Секрет попал в 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 |
Наиболее опасная ветка начинается с частичного успеха. Образ собрался, тесты прошли, а subject attestation не совпал с digest deploy. В этот момент нельзя считать выпуск безопасным из-за зелёного pipeline. Остановите продвижение, сохраните безопасные метаданные, определите последний проверенный digest и выясните, где возникло расхождение: в registry, в выборе тега, в подготовке attestation или в конфигурации deploy.
\nЕсли секрет уже попал в лог или образ, удаление строки не возвращает безопасность. Отзовите и замените credential по правилам вашей платформы. Удалите доступный артефакт, проверьте кэши и логи, а затем соберите новый образ с другим digest. Не утверждайте, что утечки не было, если проверка охватила только git и не охватила registry или CI.
\nПример с Docker — учебный. В нём нет настоящего секрета, registry, CI run, подписи или deploy; плейсхолдер `/path/to/temporary/npmrc` нельзя использовать как production-рецепт. Secret mount снижает риск записи значения в финальный слой, но не защищает от команды, которая сама печатает секрет, сохраняет его в собранный файл или отправляет его в сеть. Secret scanning помогает обнаружить известные шаблоны, но не доказывает отсутствие всех credential.
\nProvenance описывает заявленные входы и исполнителя. Оно не делает builder доверенным само по себе. Подпись подтверждает связь statement с ключом или доверенной identity, но не превращает любое утверждение в факт. Полная проверка зависит от политики организации, runner, registry, формата attestation и правил deploy. Поэтому статья не заявляет production-результатов и не заменяет проверку конкретной платформы.
\nМатериал можно считать применённым к одному выпуску, когда команда без устных пояснений показывает: commit исходников, identity builder, границу доступа к секрету, digest образа, проверенное соответствие subject этому digest и тот же digest на входе deploy. Для отрицательного пути есть запись о том, что происходит при несовпадении. Если хотя бы одного поля нет или его нельзя проверить по первичному источнику, выпуск не помечают как подтверждённый.
\n