{ "index": 163, "slug": "editorial-2023-06-field-secrets-supply-chain", "title": "Токен в образе: как проверить цепочку поставки до deploy", "excerpt": "Разбираем случай, когда токен тестовой среды оказался в контейнерном образе: как найти след, заменить небезопасный способ сборки, сопоставить digest с provenance и не объявить релиз проверенным без доказательств.", "contentHtml": "

В исходном случае токен тестовой среды случайно сохранился в контейнерном образе. Такой дефект заметен не всегда: сборка проходит, registry принимает image, а deploy запускает именно тот digest, который ожидала команда. Проблема обнаруживается позже, когда нужно ответить на два разных вопроса: где оказался секрет и из какого commit получился запущенный образ.

\n

Эти вопросы нельзя закрыть одной проверкой. Строка в Dockerfile может оставить секрет в слое, но отсутствие строки в Dockerfile не доказывает отсутствие значения в кеше, логе или артефакте CI. Тег образа показывает удобное имя, но не фиксирует его содержимое. Подписанная декларация подтверждает подписанный объект только после проверки доверия и subject, а не сам факт deploy.

\n

Практический критерий такой: выпуск можно считать проверенным только после сопоставления source revision, build platform, digest образа, subject attestation и deploy input. Если одно звено недоступно, статус должен остаться not-verified, а следующий шаг — ручной проверкой или остановкой выпуска.

\n

Симптом и границы расследования

\n

Начните с точного образа, который был выбран для deploy. Запишите полное имя registry, тег, digest, идентификатор запуска сборки и окружение. Не скачивайте неизвестный образ на рабочую машину без разрешения владельца registry: для расследования лучше использовать изолированный runner или копию с ограниченным доступом.

\n

Затем разделите наблюдения и выводы. Запись deploy доказывает, какое значение передали оркестратору. Она не доказывает commit и не объясняет, кто собрал image. Запись CI показывает запуск job, но её имя не является удостоверением build platform. Provenance (аттестация происхождения) описывает claims о сборке, однако доверие к ней требует проверки подписи, идентичности builder и digest subject.

\n
Что именно доказывает каждое свидетельство
EvidenceЧто подтверждаетЧего не подтверждает
Deploy manifestКакой input передали на развёртываниеКак получен image и где хранился секрет
Registry digestКонкретное содержимое image objectCommit, builder и безопасность содержимого
Build log и run IDФакт запуска конкретной jobЧто все шаги выполнила доверенная платформа
Provenance attestationЗаявленные builder, параметры и subjectИстину claims без проверки доверия
Image history и логиВозможные следы команд и выводаОтсутствие секрета во всех кешах и артефактах
\n

Как секрет попадает в образ

\n

Самый опасный путь — передать credential как ARG, записать его через ENV или подставить в команду RUN. Значение может оказаться в истории инструкций или в слое, который позже попадёт в registry. Даже если финальный файл удалить, предыдущий слой не исчезает автоматически.

\n

Небезопасный фрагмент выглядит так:

\n
ARG REGISTRY_TOKEN\nRUN curl -H \"Authorization: Bearer $REGISTRY_TOKEN\" \\\n    https://registry.example.invalid/private.tar.gz \\\n    -o /tmp/private.tar.gz\nRUN rm /tmp/private.tar.gz
\n

Это не доказательство того, что конкретный token уже утёк, а пример механизма риска. Проверять нужно image history, логи CI, экспортированные артефакты, кеш сборки и права доступа к registry. Если credential действительно был доступен посторонним, удаление строки из Dockerfile не отзывает его: сначала ограничьте доступ, затем замените секрет по процедуре владельца.

\n

Безопасное воспроизведение

\n

Для локальной проверки используйте заведомо фиктивную строку. BuildKit предоставляет секрет только инструкции сборки и не добавляет его автоматически в финальный слой. Важно, чтобы сама команда не печатала окружение и не копировала каталог с mounted secret в output.

\n
# Dockerfile: секрет читается только внутри одной RUN-инструкции\n# syntax=docker/dockerfile:1\nFROM alpine:3.20\nRUN --mount=type=secret,id=demo_token \\\n    test \"$(cat /run/secrets/demo_token)\" = \"dummy-for-local-check\"\nCMD [\"sh\", \"-c\", \"echo image-ok\"]\n\n# Запуск из каталога с этим Dockerfile; значение намеренно фиктивное\nDEMO_TOKEN=dummy-for-local-check \\\n  docker buildx build --progress=plain \\\n  --secret id=demo_token,env=DEMO_TOKEN \\\n  --tag supply-chain-demo:secret-mount --load .\n\n# После сборки: проверить историю, но не считать её полным аудитом\ndocker image history --no-trunc supply-chain-demo:secret-mount
\n

Ожидаемая проверка — образ собирается, контейнер не содержит файл /run/secrets/demo_token, а в истории нет фиктивного значения. Этот пример воспроизводит границу secret mount, но не проверяет ваш CI, registry или production image. В CI синтаксис передачи секрета зависит от платформы; принцип остаётся тем же: минимум прав, короткое время доступа и отсутствие значения в выводе.

\n

Digest связывает артефакт с deploy

\n

Тег вроде release или latest — это изменяемое имя. Для расследования нужен digest, то есть контентный идентификатор вида sha256:.... Он позволяет сравнить один и тот же image object в registry и deploy, но не рассказывает его историю.

\n
IMAGE=registry.example.invalid/team/app:release\n\n# Получить digest и метаданные из доступной копии image\ndocker image inspect \"$IMAGE\" \\\n  --format '{{json .RepoDigests}}'\ndocker image history --no-trunc \"$IMAGE\"\n\n# Для deploy используйте тот же digest, а не только тег\n# registry.example.invalid/team/app@sha256:<полный-digest>
\n

Сравните значение в deploy manifest посимвольно с digest, который вы получили для того же registry и платформы. Для multi-platform image уточните, сравниваете ли вы digest manifest list или digest конкретного platform image. Смешение этих уровней создаёт ложное несовпадение или, хуже, проверяет не тот объект.

\n
\"Дерево
Порядок проверки не превращает deploy-событие в provenance: сначала нужен общий digest, затем доверенная аттестация и отдельная проверка secret boundary.
\n

Как сопоставить provenance

\n

В модели SLSA attestation описывает, что build platform произвела subject через заданное определение сборки. Для практической проверки нужны как минимум четыре поля: digest subject, идентификатор builder, внешние параметры сборки и зафиксированные зависимости. Commit должен быть представлен в подходящем поле или зависимости именно той аттестации, которую вы проверяете.

\n

Сначала проверьте подпись и корень доверия. Затем убедитесь, что subject совпадает с digest образа из deploy. После этого сравните builder identity и commit с ожидаемыми значениями. Нельзя менять порядок на «нашли удобную декларацию и подогнали под неё deploy»: декларация другого digest может быть корректной сама по себе и бесполезной для текущего выпуска.

\n
# Общий шаблон Sigstore; укажите policy вашей организации\ncosign verify-attestation \\\n  --certificate-oidc-issuer https://issuer.example.invalid \\\n  --certificate-identity-regexp 'https://ci.example.invalid/.*' \\\n  oci://ghcr.io/ORG/IMAGE:release-123 \\\n  -R ORG/REPO\n\n# Если attestation найдена, отдельно сверить её subject с тем же digest.\n# Команда сама по себе не доказывает соответствие вашему release policy.
\n

Точные флаги зависят от способа подписи: key-based, keyless, корпоративный root или другой trust policy. Если verification не может проверить identity или subject, результат — не «сборка вредоносна», а «данных недостаточно для заявленного вывода». Это важная граница: отрицательный audit и доказанная компрометация — разные события.

\n

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

\n

Хорошая проверка полезна именно в момент отказа. Подставьте в тестовый manifest другой digest и убедитесь, что policy отклоняет его. Возьмите attestation от другого образа и проверьте, что subject не принимается. Запустите сборку без доступного secret store: она должна завершиться контролируемой ошибкой, а не тихо собрать публичный output без нужного шага.

\n

Для утечки есть отдельный отрицательный сценарий. Если маркер или credential обнаружен в логе, слое, кеше или артефакте, остановите публикацию, ограничьте доступ к объектам и отзовите credential. Не полагайтесь на маскирование в логе: редактирование вывода не удаляет значение из файла, слоя или уже скачанной копии.

\n
  1. Зафиксируйте симптом. Сохраните run ID, полное имя образа, deploy manifest и время, не копируя значение секрета в ticket.
  2. Изолируйте копию. Работайте с образом и логами в окружении с ограниченным доступом; не публикуйте подозрительный tarball как артефакт.
  3. Найдите след. Проверьте Dockerfile, build args, environment, команды RUN, history, кеши, логи и exported artifacts.
  4. Заморозьте спорный выпуск. Пока digest, subject или builder identity не совпадают, не продвигайте образ дальше.
  5. Замените credential. При подтверждённой утечке отзовите старый токен и проверьте, где ещё использовалась его копия.
  6. Исправьте сборку. Перенесите секрет в механизм secret mount или аналог конкретного CI и запретите печать окружения.
  7. Повторите проверку. Сравните новый digest с deploy input, provenance и expected commit, затем прогоните отрицательные сценарии.
\n

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

\n

Расследование можно закрывать, когда одна запись выпуска отвечает на пять вопросов: какой commit вошёл в build, какая доверенная platform выполнила его, какой digest получен, какой subject attestation совпадает с этим digest и какой digest использовал deploy. Отдельно должна быть запись о границе секрета: где он был разрешён, какие места проверены и какой credential остался действующим.

\n

Этот критерий не означает, что image безопасен во всех смыслах. Он лишь делает происхождение и секретный риск проверяемыми. Сканирование уязвимостей, лицензий, зависимостей, прав registry и runtime policy остаётся отдельными контролями. Если не хватает одного поля, честный результат — manual review required, а не зелёная галочка по совпадению тега.

\n

Ограничения применимости

\n

Пример рассчитан на Linux-контейнер и BuildKit с поддержкой secret mounts. Legacy builder, Windows-контейнеры, другой CI или multi-platform registry могут иметь иной синтаксис и другую семантику кеша. Команды с доменом example.invalid — шаблон: они не обращаются к реальному registry и не дают production-результата.

\n

Image history — полезный источник следов, но не доказательство чистоты: значение могло попасть в кеш, артефакт, рабочий каталог runner или внешний сервис. Provenance — утверждение, которое нужно проверять с выбранными корнями доверия и policy. Подпись гарантирует целостность подписанного объекта в рамках модели доверия, но не безопасность исходного кода и не факт его deploy.

\n

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

" }