{ "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, а следующий шаг — ручной проверкой или остановкой выпуска.
Начните с точного образа, который был выбран для 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 object | Commit, builder и безопасность содержимого |
| Build log и run ID | Факт запуска конкретной job | Что все шаги выполнила доверенная платформа |
| Provenance attestation | Заявленные builder, параметры и subject | Истину claims без проверки доверия |
| Image history и логи | Возможные следы команд и вывода | Отсутствие секрета во всех кешах и артефактах |
Самый опасный путь — передать credential как ARG, записать его через ENV или подставить в команду RUN. Значение может оказаться в истории инструкций или в слое, который позже попадёт в registry. Даже если финальный файл удалить, предыдущий слой не исчезает автоматически.
Небезопасный фрагмент выглядит так:
\nARG 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Для локальной проверки используйте заведомо фиктивную строку. 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 синтаксис передачи секрета зависит от платформы; принцип остаётся тем же: минимум прав, короткое время доступа и отсутствие значения в выводе.
Тег вроде release или latest — это изменяемое имя. Для расследования нужен digest, то есть контентный идентификатор вида sha256:.... Он позволяет сравнить один и тот же image object в registry и deploy, но не рассказывает его историю.
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В модели SLSA attestation описывает, что build platform произвела subject через заданное определение сборки. Для практической проверки нужны как минимум четыре поля: digest subject, идентификатор builder, внешние параметры сборки и зафиксированные зависимости. Commit должен быть представлен в подходящем поле или зависимости именно той аттестации, которую вы проверяете.
Сначала проверьте подпись и корень доверия. Затем убедитесь, что 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Хорошая проверка полезна именно в момент отказа. Подставьте в тестовый manifest другой digest и убедитесь, что policy отклоняет его. Возьмите attestation от другого образа и проверьте, что subject не принимается. Запустите сборку без доступного secret store: она должна завершиться контролируемой ошибкой, а не тихо собрать публичный output без нужного шага.
\nДля утечки есть отдельный отрицательный сценарий. Если маркер или credential обнаружен в логе, слое, кеше или артефакте, остановите публикацию, ограничьте доступ к объектам и отзовите credential. Не полагайтесь на маскирование в логе: редактирование вывода не удаляет значение из файла, слоя или уже скачанной копии.
\nРасследование можно закрывать, когда одна запись выпуска отвечает на пять вопросов: какой commit вошёл в build, какая доверенная platform выполнила его, какой digest получен, какой subject attestation совпадает с этим digest и какой digest использовал deploy. Отдельно должна быть запись о границе секрета: где он был разрешён, какие места проверены и какой credential остался действующим.
\nЭтот критерий не означает, что image безопасен во всех смыслах. Он лишь делает происхождение и секретный риск проверяемыми. Сканирование уязвимостей, лицензий, зависимостей, прав registry и runtime policy остаётся отдельными контролями. Если не хватает одного поля, честный результат — manual review required, а не зелёная галочка по совпадению тега.
Пример рассчитан на Linux-контейнер и BuildKit с поддержкой secret mounts. Legacy builder, Windows-контейнеры, другой CI или multi-platform registry могут иметь иной синтаксис и другую семантику кеша. Команды с доменом example.invalid — шаблон: они не обращаются к реальному registry и не дают production-результата.
Image history — полезный источник следов, но не доказательство чистоты: значение могло попасть в кеш, артефакт, рабочий каталог runner или внешний сервис. Provenance — утверждение, которое нужно проверять с выбранными корнями доверия и policy. Подпись гарантирует целостность подписанного объекта в рамках модели доверия, но не безопасность исходного кода и не факт его deploy.
\n