{ "index": 165, "slug": "editorial-2023-06-practice-secrets-supply-chain", "title": "Секрет в CI и digest образа: как проверить цепочку поставки", "excerpt": "Разбираем выпуск, в котором pipeline зелёный, но непонятно, какой commit и образ попали в deploy. Показываем границу секрета, проверку provenance и отрицательный путь.", "contentHtml": "

Симптом выглядит так: зелёный pipeline не отвечает на главный вопрос расследования: что именно сейчас запущено. В записи о релизе может быть только тег build-123, хотя тег допускает переназначение. При этом команде нужно связать четыре факта: commit исходников, identity сборщика, digest образа и вход deploy. Если на шаге сборки использовали токен, добавляется пятый вопрос: где его значение могло сохраниться.

\n

Разберём типовой выпуск как цепочку evidence — проверяемых свидетельств, а не как список названий инструментов. Секрет должен быть доступен только нужной команде и не попасть в результат. Attestation должна относиться к тому же digest, который запускает deploy. В конце получится короткий контрольный маршрут, который можно повторить на CI без доступа к значениям секретов.

\n

Сначала фиксируем контракт выпуска

\n

У выпуска должен быть один неизменяемый ключ — digest образа, а рядом с ним хранятся происхождение и решение о выкладке. Ветка и тег удобны для поиска, но не заменяют commit и digest: ветка движется, тег можно переиспользовать. Commit отвечает на вопрос «какие исходники взяли», digest — «какой результат собрали», а provenance — «какой builder заявил, как этот результат получил».

\n
{\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}
\n

Идентификаторы в примере условные. В настоящем CI запись должна ссылаться на конкретный run, registry и commit, а не на текстовое поле, которое можно исправить вручную. Поле secretUse фиксирует способ доступа, но само по себе не доказывает отсутствие значения в логах, кэше или артефактах. Для этого нужны отдельные проверки.

\n

Секрет на сборке: ссылка, а не значение

\n

Закрытая зависимость иногда требует credential во время npm ci, pip install или скачивания приватного репозитория. BuildKit secret mount делает значение доступным конкретной инструкции и не записывает его в финальный слой автоматически. Это отличается от ARG TOKEN и ENV TOKEN: Docker предупреждает, что build arguments и environment variables не подходят для передачи секретов, а аргументы могут оказаться в history или provenance.

\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,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
\n

Вызов сборки передаёт путь к файлу секрета, а не само значение в аргументе командной строки:

\n
docker buildx build \\\n  --secret type=file,id=npmrc,src=/runner/secrets/npmrc \\\n  --tag registry.example.invalid/payments/web:abc123 \\\n  --push .
\n

Путь /runner/secrets/npmrc — контракт с вашим secret manager или runner, а не команда создания настоящего credential. В CI запрещаем печать файла, добавление его в build context и копирование в /app. Даже корректный mount не спасает команду, которая делает cat /run/secrets/npmrc, пишет ответ приватного сервера в артефакт или оставляет credential в debug-логе.

\n

Digest вместо изменяемого тега

\n

Digest — криптографический идентификатор содержимого образа. Тег можно переназначить, поэтому его оставляют человекочитаемым алиасом, а для передачи между registry, attestation и deploy используют ссылку вида image@sha256:.... Сразу после push сохраните digest из registry и передайте именно его следующему этапу.

\n
# Посмотреть digest опубликованного тега\ndocker buildx imagetools inspect registry.example.invalid/payments/web:abc123\n\n# Проверить, что registry отдаёт именно зафиксированный объект\ndocker pull registry.example.invalid/payments/web@sha256:7f00000000000000000000000000000000000000000000000000000000000000
\n

Команды требуют доступного registry и подставленного реального digest; значение sha256:7f... в статье — не существующий артефакт. Для multi-platform образа нужно заранее решить, что именно является входом deploy: digest manifest list или digest конкретного варианта для linux/amd64 либо linux/arm64. Сравнивать их как одну строку без этого решения нельзя.

\n
\"Схема
У каждого звена есть отдельный вопрос. Схема показывает учебный маршрут проверки и не является журналом конкретного CI-run.
\n

Attestation: заявление, которое нужно проверить

\n

Provenance описывает, где, когда и каким процессом получен артефакт. Это полезное заявление, но не автоматический сертификат безопасности. Проверяющий сначала удостоверяется в подписи по настроенному root of trust, затем сопоставляет subject с digest, проверяет ожидаемый predicateType и identity builder. После криптографической проверки остаётся ещё политический вопрос: разрешены ли этот репозиторий, workflow, commit и окружение.

\n

Для SLSA-подобной проверки порядок важен. Если subject относится к sha256:91..., а deploy запускает sha256:7f..., валидная подпись не исправляет расхождение. Если builder неизвестен политике, запись о provenance нельзя считать достаточным основанием для выпуска. Если проверка относится к тегу, зафиксируйте разрешённый digest рядом с результатом, иначе между проверкой и deploy возможна подмена тега.

\n
# Пример для GitHub Container Registry и GitHub CLI.\n# ORG, REPO и IMAGE заменяются значениями проекта.\ngh attestation verify \\\n  oci://ghcr.io/ORG/IMAGE:release-123 \\\n  -R ORG/REPO
\n

Команда проверяет доступную GitHub attestation для указанного образа, но не знает вашу политику автоматически. После неё отдельно сверяем repository, workflow, commit, builder identity и digest с deploy manifest. В другой CI-платформе остаётся тот же порядок, меняются формат attestation и инструмент проверки.

\n

Матрица симптомов и решений

\n
От наблюдаемого сигнала к проверяемому действию
СимптомГипотезаПроверкаРешение
Deploy содержит только тегТег переназначили после сборкиПолучить фактический digest из runtime и сравнить с registryПеревести manifest на digest и сохранить его в release evidence
Attestation есть, subject другойПроверяли один output, запускают другойСравнить полные строки subject и deploy inputОстановить выпуск, выбрать проверенный digest и найти место расхождения
В Dockerfile есть ARG TOKENCredential попал в history или metadataПроверить docker history, metadata и историю CIОтозвать токен, заменить передачу на secret mount, собрать новый образ
В логе виден фрагмент токенаКоманда или debug напечатали секретПроверить весь run, артефакты, кэш и системы логированияНемедленно отозвать credential и повторить выпуск с новым digest
Builder не входит в root of trustProvenance подписана неизвестным исполнителемСверить builder identity и ключ с политикой проектаНе принимать выпуск; сначала зарегистрировать доверенный путь или изменить builder
\n

Воспроизводимый контроль в CI

\n

Проверка должна завершаться сравнением значений, а не только просмотром зелёного статуса. Следующий фрагмент не публикует секрет и не меняет кластер: он моделирует последний decision gate перед deploy.

\n
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.
\n

В реальном job переменные должны приходить из проверенных outputs, а не из ручного ввода. Добавьте отрицательный тест: намеренно подставьте другой digest и убедитесь, что test возвращает ненулевой код, job останавливается, а deploy не вызывается. Отдельно проверяйте commit и builder, потому что совпадение двух строк digest не доказывает происхождение образа.

\n

Порядок расследования

\n
  1. Зафиксируйте точный image reference, который runtime получил при deploy. Если запись содержит тег, разрешите его в digest и отметьте время проверки.
  2. Найдите commit, переданный в сборку, и ссылку на конкретный CI run. Не заменяйте commit названием ветки.
  3. Определите builder identity и проверьте, входит ли она в настроенный root of trust.
  4. Проверьте все места использования секрета: secret manager, шаг сборки, stdout, cache, слои и опубликованные артефакты.
  5. Проверьте подпись attestation, затем predicateType, subject digest и заявленные входы provenance.
  6. Сравните verified digest с тем же digest в deploy manifest. Для multi-platform публикации зафиксируйте уровень manifest, на котором сравниваете.
  7. Запишите результат каждого шага: подтверждено, не подтверждено или неприменимо. Не превращайте неизвестное поле в зелёный статус.
\n

Отрицательный путь: выпуск нужно остановить

\n

Представим, что тесты прошли, образ опубликован, но attestation относится к sha256:91..., а deploy manifest содержит sha256:7f.... Сохраняем логи и metadata, блокируем promotion и выясняем, где возник разрыв: push создал другой output, тег разрешился иначе, attestation выпустили для соседнего артефакта или manifest собрали из старого значения.

\n

Если credential попал в лог, слой или артефакт, удаление строки не возвращает его безопасность. Отзовите и замените credential по правилам вашей платформы, ограничьте доступ к копиям, проверьте retention и кэши, затем выпустите новый образ с новым digest. Результат расследования должен говорить, какие поверхности проверены; фраза «секрет не утёк» без охвата CI, registry и артефактов слишком сильна.

\n

Границы применимости

\n

Примеры используют Docker BuildKit, registry, GitHub CLI и SLSA-термины. В Jenkins, GitLab, Yandex CI или закрытом registry будут другими команды, форматы attestation и политика доверия. Перед внедрением сверяйте версию Dockerfile frontend, возможности runner, режим кэширования, права registry и способ, которым runtime разрешает multi-platform image.

\n

Secret mount уменьшает вероятность записи значения в финальный слой, но не делает процесс невосприимчивым к вредоносной команде, debug-выводу или компрометации builder. Digest защищает от подмены содержимого по этому адресу, но не доказывает, что исходный код безопасен. Attestation связывает заявление с артефактом и builder; SLSA отдельно оговаривает доверие к самой build-платформе. Поэтому модель не заменяет threat model, ротацию credential, контроль прав и независимую проверку runner.

\n

Критерий готовности

\n

Выпуск можно принять, когда без устных пояснений доступны commit, ссылка на CI run и builder identity; секрет получен через разрешённую границу и не найден в логах, слоях или артефактах; подпись и provenance проверены; subject совпадает с digest; тот же digest записан в deploy input. Для несовпадения есть автоматический fail-closed тест и понятный владелец расследования. Если поле недоступно или проверка охватывает только одну поверхность, статус выпуска остаётся неподтверждённым.

\n

Проверяемые источники

\n" }