Files

8 lines
20 KiB
JSON
Raw Permalink 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": "Разбираем выпуск, в котором 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 &quot;revision&quot;: &quot;abc123...&quot;,\n &quot;builderRun&quot;: &quot;https://ci.example.invalid/runs/8472&quot;,\n &quot;imageDigest&quot;: &quot;registry.example.invalid/payments/api@sha256:7f...&quot;,\n &quot;attestationSubject&quot;: &quot;registry.example.invalid/payments/api@sha256:7f...&quot;,\n &quot;deployInput&quot;: &quot;registry.example.invalid/payments/api@sha256:7f...&quot;,\n &quot;secretUse&quot;: &quot;mounted for npm ci; value is not evidence&quot;\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=&quot;registry.example.invalid/payments/api@sha256:7f...&quot;\nDEPLOY_DIGEST=&quot;registry.example.invalid/payments/api@sha256:7f...&quot;\n\ntest &quot;$ATTESTED_DIGEST&quot; = &quot;$DEPLOY_DIGEST&quot;\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>"
}