8 lines
15 KiB
JSON
8 lines
15 KiB
JSON
{
|
||
"index": 165,
|
||
"slug": "editorial-2023-06-practice-secrets-supply-chain",
|
||
"title": "Секрет в CI и digest образа: как связать звенья цепочки поставки",
|
||
"excerpt": "Секрет не должен становиться частью образа, а deploy должен ссылаться на тот же digest, который прошёл проверки. Разбираем границы, evidence и отрицательный путь.",
|
||
"contentHtml": "<p>В релизе есть запись о deploy, но никто не может быстро ответить на четыре вопроса: из какой ревизии собрали образ, какой процесс его собрал, использовал ли build секрет и какой digest действительно запустили. Симптом часто выглядит безобидно: pipeline зелёный, сервис работает, а расследование останавливается на фразе «образ собрал CI». Цена ошибки появляется позже. Если токен попал в слой образа или deploy взял соседний тег, команда не может надёжно определить затронутый артефакт, отозвать доступ и объяснить происхождение выпуска.</p>\n<p>Тезис простой: цепочку поставки нужно проверять как связь фактов, а не как набор названий инструментов. Ревизия исходников, идентичность сборщика, граница секрета, digest образа, утверждение о происхождении и вход deploy должны иметь общий ключ и понятного владельца проверки. Декларация «образ подписан» не заменяет сопоставление subject с digest. Наличие переменной `TOKEN` в CI не доказывает, что её значение не попало в лог или слой образа.</p>\n<h2>Механизм: секрет проходит этап, но не должен проходить в результат</h2>\n<p>Секрет нужен build только на коротком шаге: например, чтобы скачать закрытую зависимость. Процесс должен получить ссылку на секрет, использовать её внутри команды и не записать значение в переменную окружения, слой, кэш или stdout. Образ после этого содержит приложение и публичные настройки, но не credential. В Docker BuildKit для такой границы используют secret mount. Build argument и обычная переменная окружения для этого не подходят: они могут сохраниться в истории сборки или финальном образе.</p>\n<pre><code># 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</code></pre>\n<p>Команда запуска должна передать секрет отдельно от контекста сборки. В учебном примере ниже имя файла условное. Не подставляйте настоящий токен в статью, shell history или CI log.</p>\n<pre><code>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.</code></pre>\n<p>Этот код показывает границу передачи. Он не доказывает, что конкретный runner настроен безопасно, что registry доверенный и что deploy использовал правильный digest. Эти утверждения требуют наблюдаемых записей из вашей среды.</p>\n<h2>Один пример связи фактов</h2>\n<p>Представьте выпуск `release-123`. Исходная ревизия — `git:abc123`. Сборщик сообщает identity `ci/build-prod`. Registry возвращает digest `sha256:7f...`. Attestation имеет subject с тем же digest и ссылается на `git:abc123`. Deploy получает не тег `build-123`, а полный digest. Тогда расследование может пройти по одной цепочке. Если хотя бы одно звено хранит только свободный текст, связь становится гипотезой.</p>\n<p>Тег удобен для человека, но изменяем. Digest адресует конкретный результат. Поэтому тег можно показывать в интерфейсе, а проверяемым входом выкладки считать digest. Если attestation относится к `sha256:91...`, а deploy запускает `sha256:7f...`, выпуск нужно остановить. Нельзя исправить несовпадение новым комментарием в release.</p>\n<figure><img src=\"/assets/editorial/2023/secrets-supply-chain-2023-trust-boundaries.svg\" alt=\"Границы доверия между исходной ревизией, CI, секретом, образом и deploy\" loading=\"lazy\" /><figcaption>Карта границ доверия. Она помогает назвать проверяемые факты, но не является журналом реального CI и не подтверждает подпись образа.</figcaption></figure>\n<h2>Симптомы и действия</h2>\n<div class=\"table-scroll\"><table><caption>Как перейти от сигнала к проверке</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>В логах виден фрагмент токена</td><td>Секрет попал в stdout, debug или командную строку</td><td>Проверить логи шага и маскирование, затем поискать значение в слоях и артефактах</td><td>Отозвать credential, очистить путь вывода и повторить сборку без утечки</td></tr><tr><td>В Dockerfile есть `ARG TOKEN`</td><td>Секрет передаётся как параметр и может остаться в истории</td><td>Проверить history и metadata образа</td><td>Перейти на secret mount и выпустить новый digest</td></tr><tr><td>Attestation и deploy ссылаются на разные digest</td><td>Выкладка использует изменяемый тег или другой output</td><td>Сравнить точные subject и deploy input</td><td>Остановить выпуск, выбрать проверенный digest, выяснить источник расхождения</td></tr><tr><td>Есть подпись, но нет записи о builder</td><td>Проверяют целостность statement, но не происхождение сборки</td><td>Проверить identity подписанта и поля provenance</td><td>Разделить проверку подписи, builder и исходной ревизии</td></tr><tr><td>После deploy нельзя найти исходную ревизию</td><td>Release хранит только номер задачи или короткий тег</td><td>Сверить metadata образа, CI run и commit</td><td>Сделать revision обязательным полем evidence</td></tr></tbody></table></div>\n<h2>Порядок действий</h2>\n<ol><li>Выберите один выпуск и зафиксируйте его точный deploy input. Если система принимает тег, получите digest, который фактически использовал runtime.</li><li>Найдите исходную ревизию, из которой собрали этот digest. Не подменяйте её веткой: ветка меняется, commit остаётся идентификатором состояния.</li><li>Определите identity builder и сохраните ссылку на конкретный запуск. Запись «собрано CI» недостаточна.</li><li>Проверьте границу секрета: где его запросили, какой шаг получил доступ и какие файлы, слои, логи и кэши могли его сохранить.</li><li>Сопоставьте subject attestation с digest образа. Затем отдельно проверьте подпись, доверенную identity и ожидаемую ревизию.</li><li>Сравните этот же digest с входом deploy. При несовпадении не продолжайте выпуск и не заменяйте digest повторным тегированием.</li><li>Зафиксируйте результат и владельца следующей проверки. Для каждого неизвестного факта оставьте статус «не подтверждено».</li></ol>\n<h2>Отрицательный путь: что делать при разрыве</h2>\n<p>Наиболее опасная ветка начинается с частичного успеха. Образ собрался, тесты прошли, а subject attestation не совпал с digest deploy. В этот момент нельзя считать выпуск безопасным из-за зелёного pipeline. Остановите продвижение, сохраните безопасные метаданные, определите последний проверенный digest и выясните, где возникло расхождение: в registry, в выборе тега, в подготовке attestation или в конфигурации deploy.</p>\n<p>Если секрет уже попал в лог или образ, удаление строки не возвращает безопасность. Отзовите и замените credential по правилам вашей платформы. Удалите доступный артефакт, проверьте кэши и логи, а затем соберите новый образ с другим digest. Не утверждайте, что утечки не было, если проверка охватила только git и не охватила registry или CI.</p>\n<h2>Ограничения</h2>\n<p>Пример с Docker — учебный. В нём нет настоящего секрета, registry, CI run, подписи или deploy; плейсхолдер `/path/to/temporary/npmrc` нельзя использовать как production-рецепт. Secret mount снижает риск записи значения в финальный слой, но не защищает от команды, которая сама печатает секрет, сохраняет его в собранный файл или отправляет его в сеть. Secret scanning помогает обнаружить известные шаблоны, но не доказывает отсутствие всех credential.</p>\n<p>Provenance описывает заявленные входы и исполнителя. Оно не делает builder доверенным само по себе. Подпись подтверждает связь statement с ключом или доверенной identity, но не превращает любое утверждение в факт. Полная проверка зависит от политики организации, runner, registry, формата attestation и правил deploy. Поэтому статья не заявляет production-результатов и не заменяет проверку конкретной платформы.</p>\n<h2>Критерий готовности</h2>\n<p>Материал можно считать применённым к одному выпуску, когда команда без устных пояснений показывает: commit исходников, identity builder, границу доступа к секрету, digest образа, проверенное соответствие subject этому digest и тот же digest на входе deploy. Для отрицательного пути есть запись о том, что происходит при несовпадении. Если хотя бы одного поля нет или его нельзя проверить по первичному источнику, выпуск не помечают как подтверждённый.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.docker.com/build/building/secrets/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Build secrets</a> — официальное описание secret и SSH mounts и предупреждение о build arguments и environment variables для секретов.</li><li><a href=\"https://slsa.dev/spec/v1.2/provenance\" target=\"_blank\" rel=\"noopener noreferrer\">SLSA: Provenance v1.2</a> — официальное описание полей provenance и связи утверждения с subject; документ не подтверждает ваш pipeline.</li><li><a href=\"https://docs.github.com/en/code-security/concepts/secret-security/push-protection\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: Push protection</a> — официальное описание блокировки обнаруженных секретов до попадания в репозиторий; функция не заменяет проверку CI, registry и образов.</li></ul>"
|
||
}
|