Files
progcode/editorial/agent-rewrites/165.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
15 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>"
}