8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 165,
|
||
"slug": "editorial-2023-06-practice-secrets-supply-chain",
|
||
"title": "Секрет в CI и digest образа: как проверить цепочку поставки",
|
||
"excerpt": "Разбираем выпуск, в котором pipeline зелёный, но непонятно, какой commit и образ попали в deploy. Показываем границу секрета, проверку provenance и отрицательный путь.",
|
||
"contentHtml": "<p>Симптом выглядит так: зелёный pipeline не отвечает на главный вопрос расследования: что именно сейчас запущено. В записи о релизе может быть только тег <code>build-123</code>, хотя тег допускает переназначение. При этом команде нужно связать четыре факта: commit исходников, identity сборщика, digest образа и вход deploy. Если на шаге сборки использовали токен, добавляется пятый вопрос: где его значение могло сохраниться.</p>\n<p>Разберём типовой выпуск как цепочку evidence — проверяемых свидетельств, а не как список названий инструментов. Секрет должен быть доступен только нужной команде и не попасть в результат. Attestation должна относиться к тому же digest, который запускает deploy. В конце получится короткий контрольный маршрут, который можно повторить на CI без доступа к значениям секретов.</p>\n<h2>Сначала фиксируем контракт выпуска</h2>\n<p>У выпуска должен быть один неизменяемый ключ — digest образа, а рядом с ним хранятся происхождение и решение о выкладке. Ветка и тег удобны для поиска, но не заменяют commit и digest: ветка движется, тег можно переиспользовать. Commit отвечает на вопрос «какие исходники взяли», digest — «какой результат собрали», а provenance — «какой builder заявил, как этот результат получил».</p>\n<pre><code>{\n "revision": "abc123...",\n "builderRun": "https://ci.example.invalid/runs/8472",\n "imageDigest": "registry.example.invalid/payments/api@sha256:7f...",\n "attestationSubject": "registry.example.invalid/payments/api@sha256:7f...",\n "deployInput": "registry.example.invalid/payments/api@sha256:7f...",\n "secretUse": "mounted for npm ci; value is not evidence"\n}</code></pre>\n<p>Идентификаторы в примере условные. В настоящем CI запись должна ссылаться на конкретный run, registry и commit, а не на текстовое поле, которое можно исправить вручную. Поле <code>secretUse</code> фиксирует способ доступа, но само по себе не доказывает отсутствие значения в логах, кэше или артефактах. Для этого нужны отдельные проверки.</p>\n<h2>Секрет на сборке: ссылка, а не значение</h2>\n<p>Закрытая зависимость иногда требует credential во время <code>npm ci</code>, <code>pip install</code> или скачивания приватного репозитория. BuildKit secret mount делает значение доступным конкретной инструкции и не записывает его в финальный слой автоматически. Это отличается от <code>ARG TOKEN</code> и <code>ENV TOKEN</code>: Docker предупреждает, что build arguments и environment variables не подходят для передачи секретов, а аргументы могут оказаться в history или provenance.</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,required=true \\\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>Вызов сборки передаёт путь к файлу секрета, а не само значение в аргументе командной строки:</p>\n<pre><code>docker buildx build \\\n --secret type=file,id=npmrc,src=/runner/secrets/npmrc \\\n --tag registry.example.invalid/payments/web:abc123 \\\n --push .</code></pre>\n<p>Путь <code>/runner/secrets/npmrc</code> — контракт с вашим secret manager или runner, а не команда создания настоящего credential. В CI запрещаем печать файла, добавление его в build context и копирование в <code>/app</code>. Даже корректный mount не спасает команду, которая делает <code>cat /run/secrets/npmrc</code>, пишет ответ приватного сервера в артефакт или оставляет credential в debug-логе.</p>\n<h2>Digest вместо изменяемого тега</h2>\n<p>Digest — криптографический идентификатор содержимого образа. Тег можно переназначить, поэтому его оставляют человекочитаемым алиасом, а для передачи между registry, attestation и deploy используют ссылку вида <code>image@sha256:...</code>. Сразу после push сохраните digest из registry и передайте именно его следующему этапу.</p>\n<pre><code># Посмотреть digest опубликованного тега\ndocker buildx imagetools inspect registry.example.invalid/payments/web:abc123\n\n# Проверить, что registry отдаёт именно зафиксированный объект\ndocker pull registry.example.invalid/payments/web@sha256:7f00000000000000000000000000000000000000000000000000000000000000</code></pre>\n<p>Команды требуют доступного registry и подставленного реального digest; значение <code>sha256:7f...</code> в статье — не существующий артефакт. Для multi-platform образа нужно заранее решить, что именно является входом deploy: digest manifest list или digest конкретного варианта для <code>linux/amd64</code> либо <code>linux/arm64</code>. Сравнивать их как одну строку без этого решения нельзя.</p>\n<figure><img src=\"/assets/editorial/2023/secrets-supply-chain-2023-trust-boundaries.svg\" alt=\"Схема проверки: commit и identity CI связаны с границей секрета, digest образа, attestation и решением deploy\" loading=\"lazy\" /><figcaption>У каждого звена есть отдельный вопрос. Схема показывает учебный маршрут проверки и не является журналом конкретного CI-run.</figcaption></figure>\n<h2>Attestation: заявление, которое нужно проверить</h2>\n<p>Provenance описывает, где, когда и каким процессом получен артефакт. Это полезное заявление, но не автоматический сертификат безопасности. Проверяющий сначала удостоверяется в подписи по настроенному root of trust, затем сопоставляет <code>subject</code> с digest, проверяет ожидаемый <code>predicateType</code> и identity builder. После криптографической проверки остаётся ещё политический вопрос: разрешены ли этот репозиторий, workflow, commit и окружение.</p>\n<p>Для SLSA-подобной проверки порядок важен. Если subject относится к <code>sha256:91...</code>, а deploy запускает <code>sha256:7f...</code>, валидная подпись не исправляет расхождение. Если builder неизвестен политике, запись о provenance нельзя считать достаточным основанием для выпуска. Если проверка относится к тегу, зафиксируйте разрешённый digest рядом с результатом, иначе между проверкой и deploy возможна подмена тега.</p>\n<pre><code># Пример для GitHub Container Registry и GitHub CLI.\n# ORG, REPO и IMAGE заменяются значениями проекта.\ngh attestation verify \\\n oci://ghcr.io/ORG/IMAGE:release-123 \\\n -R ORG/REPO</code></pre>\n<p>Команда проверяет доступную GitHub attestation для указанного образа, но не знает вашу политику автоматически. После неё отдельно сверяем repository, workflow, commit, builder identity и digest с deploy manifest. В другой CI-платформе остаётся тот же порядок, меняются формат attestation и инструмент проверки.</p>\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>Deploy содержит только тег</td><td>Тег переназначили после сборки</td><td>Получить фактический digest из runtime и сравнить с registry</td><td>Перевести manifest на digest и сохранить его в release evidence</td></tr><tr><td>Attestation есть, subject другой</td><td>Проверяли один output, запускают другой</td><td>Сравнить полные строки <code>subject</code> и deploy input</td><td>Остановить выпуск, выбрать проверенный digest и найти место расхождения</td></tr><tr><td>В Dockerfile есть <code>ARG TOKEN</code></td><td>Credential попал в history или metadata</td><td>Проверить <code>docker history</code>, metadata и историю CI</td><td>Отозвать токен, заменить передачу на secret mount, собрать новый образ</td></tr><tr><td>В логе виден фрагмент токена</td><td>Команда или debug напечатали секрет</td><td>Проверить весь run, артефакты, кэш и системы логирования</td><td>Немедленно отозвать credential и повторить выпуск с новым digest</td></tr><tr><td>Builder не входит в root of trust</td><td>Provenance подписана неизвестным исполнителем</td><td>Сверить builder identity и ключ с политикой проекта</td><td>Не принимать выпуск; сначала зарегистрировать доверенный путь или изменить builder</td></tr></tbody></table></div>\n<h2>Воспроизводимый контроль в CI</h2>\n<p>Проверка должна завершаться сравнением значений, а не только просмотром зелёного статуса. Следующий фрагмент не публикует секрет и не меняет кластер: он моделирует последний decision gate перед deploy.</p>\n<pre><code>set -eu\nATTESTED_DIGEST="registry.example.invalid/payments/api@sha256:7f..."\nDEPLOY_DIGEST="registry.example.invalid/payments/api@sha256:7f..."\n\ntest "$ATTESTED_DIGEST" = "$DEPLOY_DIGEST"\nprintf 'attestation and deploy refer to the same digest\\n'\n\n# Дальше запускается только заранее разрешённый deploy job.\n# В manifest сохраняем полный image@sha256:... без mutable tag.</code></pre>\n<p>В реальном job переменные должны приходить из проверенных outputs, а не из ручного ввода. Добавьте отрицательный тест: намеренно подставьте другой digest и убедитесь, что <code>test</code> возвращает ненулевой код, job останавливается, а deploy не вызывается. Отдельно проверяйте commit и builder, потому что совпадение двух строк digest не доказывает происхождение образа.</p>\n<h2>Порядок расследования</h2>\n<ol><li>Зафиксируйте точный image reference, который runtime получил при deploy. Если запись содержит тег, разрешите его в digest и отметьте время проверки.</li><li>Найдите commit, переданный в сборку, и ссылку на конкретный CI run. Не заменяйте commit названием ветки.</li><li>Определите builder identity и проверьте, входит ли она в настроенный root of trust.</li><li>Проверьте все места использования секрета: secret manager, шаг сборки, stdout, cache, слои и опубликованные артефакты.</li><li>Проверьте подпись attestation, затем <code>predicateType</code>, subject digest и заявленные входы provenance.</li><li>Сравните verified digest с тем же digest в deploy manifest. Для multi-platform публикации зафиксируйте уровень manifest, на котором сравниваете.</li><li>Запишите результат каждого шага: подтверждено, не подтверждено или неприменимо. Не превращайте неизвестное поле в зелёный статус.</li></ol>\n<h2>Отрицательный путь: выпуск нужно остановить</h2>\n<p>Представим, что тесты прошли, образ опубликован, но attestation относится к <code>sha256:91...</code>, а deploy manifest содержит <code>sha256:7f...</code>. Сохраняем логи и metadata, блокируем promotion и выясняем, где возник разрыв: push создал другой output, тег разрешился иначе, attestation выпустили для соседнего артефакта или manifest собрали из старого значения.</p>\n<p>Если credential попал в лог, слой или артефакт, удаление строки не возвращает его безопасность. Отзовите и замените credential по правилам вашей платформы, ограничьте доступ к копиям, проверьте retention и кэши, затем выпустите новый образ с новым digest. Результат расследования должен говорить, какие поверхности проверены; фраза «секрет не утёк» без охвата CI, registry и артефактов слишком сильна.</p>\n<h2>Границы применимости</h2>\n<p>Примеры используют Docker BuildKit, registry, GitHub CLI и SLSA-термины. В Jenkins, GitLab, Yandex CI или закрытом registry будут другими команды, форматы attestation и политика доверия. Перед внедрением сверяйте версию Dockerfile frontend, возможности runner, режим кэширования, права registry и способ, которым runtime разрешает multi-platform image.</p>\n<p>Secret mount уменьшает вероятность записи значения в финальный слой, но не делает процесс невосприимчивым к вредоносной команде, debug-выводу или компрометации builder. Digest защищает от подмены содержимого по этому адресу, но не доказывает, что исходный код безопасен. Attestation связывает заявление с артефактом и builder; SLSA отдельно оговаривает доверие к самой build-платформе. Поэтому модель не заменяет threat model, ротацию credential, контроль прав и независимую проверку runner.</p>\n<h2>Критерий готовности</h2>\n<p>Выпуск можно принять, когда без устных пояснений доступны commit, ссылка на CI run и builder identity; секрет получен через разрешённую границу и не найден в логах, слоях или артефактах; подпись и provenance проверены; subject совпадает с digest; тот же digest записан в deploy input. Для несовпадения есть автоматический fail-closed тест и понятный владелец расследования. Если поле недоступно или проверка охватывает только одну поверхность, статус выпуска остаётся неподтверждённым.</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 instruction.</li><li><a href=\"https://docs.docker.com/reference/cli/docker/buildx/build/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: docker buildx build</a> — фиксирует синтаксис <code>--secret</code>, типы <code>file</code>/<code>env</code> и <code>RUN --mount=type=secret</code>.</li><li><a href=\"https://docs.docker.com/dhi/explore/security-concepts/digests/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Image digests</a> — объясняет отличие digest от тега, pinning по digest и нюанс manifest list для нескольких платформ.</li><li><a href=\"https://slsa.dev/spec/v1.2/verifying-artifacts\" target=\"_blank\" rel=\"noopener noreferrer\">SLSA v1.2: Verifying artifacts</a> — задаёт последовательность проверки подписи, subject, predicate type и builder identity, а также границу доверия к build-платформе.</li><li><a href=\"https://docs.github.com/en/actions/concepts/security/artifact-attestations\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: Artifact attestations</a> — описывает provenance attestations и прямо предупреждает, что attestation не гарантирует безопасность артефакта без проверки политики.</li></ul>"
|
||
}
|