8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 163,
|
||
"slug": "editorial-2023-06-field-secrets-supply-chain",
|
||
"title": "Токен в образе: как проверить цепочку поставки до deploy",
|
||
"excerpt": "Разбираем случай, когда токен тестовой среды оказался в контейнерном образе: как найти след, заменить небезопасный способ сборки, сопоставить digest с provenance и не объявить релиз проверенным без доказательств.",
|
||
"contentHtml": "<p>В исходном случае токен тестовой среды случайно сохранился в контейнерном образе. Такой дефект заметен не всегда: сборка проходит, registry принимает image, а deploy запускает именно тот digest, который ожидала команда. Проблема обнаруживается позже, когда нужно ответить на два разных вопроса: где оказался секрет и из какого commit получился запущенный образ.</p>\n<p>Эти вопросы нельзя закрыть одной проверкой. Строка в Dockerfile может оставить секрет в слое, но отсутствие строки в Dockerfile не доказывает отсутствие значения в кеше, логе или артефакте CI. Тег образа показывает удобное имя, но не фиксирует его содержимое. Подписанная декларация подтверждает подписанный объект только после проверки доверия и subject, а не сам факт deploy.</p>\n<p>Практический критерий такой: выпуск можно считать проверенным только после сопоставления source revision, build platform, digest образа, subject attestation и deploy input. Если одно звено недоступно, статус должен остаться <code>not-verified</code>, а следующий шаг — ручной проверкой или остановкой выпуска.</p>\n<h2>Симптом и границы расследования</h2>\n<p>Начните с точного образа, который был выбран для deploy. Запишите полное имя registry, тег, digest, идентификатор запуска сборки и окружение. Не скачивайте неизвестный образ на рабочую машину без разрешения владельца registry: для расследования лучше использовать изолированный runner или копию с ограниченным доступом.</p>\n<p>Затем разделите наблюдения и выводы. Запись deploy доказывает, какое значение передали оркестратору. Она не доказывает commit и не объясняет, кто собрал image. Запись CI показывает запуск job, но её имя не является удостоверением build platform. Provenance (аттестация происхождения) описывает claims о сборке, однако доверие к ней требует проверки подписи, идентичности builder и digest subject.</p>\n<table><caption>Что именно доказывает каждое свидетельство</caption><thead><tr><th scope=\"col\">Evidence</th><th scope=\"col\">Что подтверждает</th><th scope=\"col\">Чего не подтверждает</th></tr></thead><tbody><tr><td>Deploy manifest</td><td>Какой input передали на развёртывание</td><td>Как получен image и где хранился секрет</td></tr><tr><td>Registry digest</td><td>Конкретное содержимое image object</td><td>Commit, builder и безопасность содержимого</td></tr><tr><td>Build log и run ID</td><td>Факт запуска конкретной job</td><td>Что все шаги выполнила доверенная платформа</td></tr><tr><td>Provenance attestation</td><td>Заявленные builder, параметры и subject</td><td>Истину claims без проверки доверия</td></tr><tr><td>Image history и логи</td><td>Возможные следы команд и вывода</td><td>Отсутствие секрета во всех кешах и артефактах</td></tr></tbody></table>\n<h2>Как секрет попадает в образ</h2>\n<p>Самый опасный путь — передать credential как <code>ARG</code>, записать его через <code>ENV</code> или подставить в команду <code>RUN</code>. Значение может оказаться в истории инструкций или в слое, который позже попадёт в registry. Даже если финальный файл удалить, предыдущий слой не исчезает автоматически.</p>\n<p>Небезопасный фрагмент выглядит так:</p>\n<pre><code>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</code></pre>\n<p>Это не доказательство того, что конкретный token уже утёк, а пример механизма риска. Проверять нужно image history, логи CI, экспортированные артефакты, кеш сборки и права доступа к registry. Если credential действительно был доступен посторонним, удаление строки из Dockerfile не отзывает его: сначала ограничьте доступ, затем замените секрет по процедуре владельца.</p>\n<h2>Безопасное воспроизведение</h2>\n<p>Для локальной проверки используйте заведомо фиктивную строку. BuildKit предоставляет секрет только инструкции сборки и не добавляет его автоматически в финальный слой. Важно, чтобы сама команда не печатала окружение и не копировала каталог с mounted secret в output.</p>\n<pre><code># 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</code></pre>\n<p>Ожидаемая проверка — образ собирается, контейнер не содержит файл <code>/run/secrets/demo_token</code>, а в истории нет фиктивного значения. Этот пример воспроизводит границу secret mount, но не проверяет ваш CI, registry или production image. В CI синтаксис передачи секрета зависит от платформы; принцип остаётся тем же: минимум прав, короткое время доступа и отсутствие значения в выводе.</p>\n<h2>Digest связывает артефакт с deploy</h2>\n<p>Тег вроде <code>release</code> или <code>latest</code> — это изменяемое имя. Для расследования нужен digest, то есть контентный идентификатор вида <code>sha256:...</code>. Он позволяет сравнить один и тот же image object в registry и deploy, но не рассказывает его историю.</p>\n<pre><code>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></code></pre>\n<p>Сравните значение в deploy manifest посимвольно с digest, который вы получили для того же registry и платформы. Для multi-platform image уточните, сравниваете ли вы digest manifest list или digest конкретного platform image. Смешение этих уровней создаёт ложное несовпадение или, хуже, проверяет не тот объект.</p>\n<figure><img src=\"/assets/editorial/2023/secrets-supply-chain-2023-release-decision.svg\" alt=\"Дерево проверки контейнерного релиза: deploy digest сравнивается с subject аттестации, затем отдельно проверяются builder и граница секрета; при любом несоответствии выпуск переходит в ручную проверку\" loading=\"lazy\" /><figcaption>Порядок проверки не превращает deploy-событие в provenance: сначала нужен общий digest, затем доверенная аттестация и отдельная проверка secret boundary.</figcaption></figure>\n<h2>Как сопоставить provenance</h2>\n<p>В модели SLSA attestation описывает, что build platform произвела <code>subject</code> через заданное определение сборки. Для практической проверки нужны как минимум четыре поля: digest subject, идентификатор builder, внешние параметры сборки и зафиксированные зависимости. Commit должен быть представлен в подходящем поле или зависимости именно той аттестации, которую вы проверяете.</p>\n<p>Сначала проверьте подпись и корень доверия. Затем убедитесь, что subject совпадает с digest образа из deploy. После этого сравните builder identity и commit с ожидаемыми значениями. Нельзя менять порядок на «нашли удобную декларацию и подогнали под неё deploy»: декларация другого digest может быть корректной сама по себе и бесполезной для текущего выпуска.</p>\n<pre><code># Общий шаблон 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.</code></pre>\n<p>Точные флаги зависят от способа подписи: key-based, keyless, корпоративный root или другой trust policy. Если verification не может проверить identity или subject, результат — не «сборка вредоносна», а «данных недостаточно для заявленного вывода». Это важная граница: отрицательный audit и доказанная компрометация — разные события.</p>\n<h2>Отрицательный путь должен останавливать выпуск</h2>\n<p>Хорошая проверка полезна именно в момент отказа. Подставьте в тестовый manifest другой digest и убедитесь, что policy отклоняет его. Возьмите attestation от другого образа и проверьте, что subject не принимается. Запустите сборку без доступного secret store: она должна завершиться контролируемой ошибкой, а не тихо собрать публичный output без нужного шага.</p>\n<p>Для утечки есть отдельный отрицательный сценарий. Если маркер или credential обнаружен в логе, слое, кеше или артефакте, остановите публикацию, ограничьте доступ к объектам и отзовите credential. Не полагайтесь на маскирование в логе: редактирование вывода не удаляет значение из файла, слоя или уже скачанной копии.</p>\n<ol><li><strong>Зафиксируйте симптом.</strong> Сохраните run ID, полное имя образа, deploy manifest и время, не копируя значение секрета в ticket.</li><li><strong>Изолируйте копию.</strong> Работайте с образом и логами в окружении с ограниченным доступом; не публикуйте подозрительный tarball как артефакт.</li><li><strong>Найдите след.</strong> Проверьте Dockerfile, build args, environment, команды RUN, history, кеши, логи и exported artifacts.</li><li><strong>Заморозьте спорный выпуск.</strong> Пока digest, subject или builder identity не совпадают, не продвигайте образ дальше.</li><li><strong>Замените credential.</strong> При подтверждённой утечке отзовите старый токен и проверьте, где ещё использовалась его копия.</li><li><strong>Исправьте сборку.</strong> Перенесите секрет в механизм secret mount или аналог конкретного CI и запретите печать окружения.</li><li><strong>Повторите проверку.</strong> Сравните новый digest с deploy input, provenance и expected commit, затем прогоните отрицательные сценарии.</li></ol>\n<h2>Критерий готовности</h2>\n<p>Расследование можно закрывать, когда одна запись выпуска отвечает на пять вопросов: какой commit вошёл в build, какая доверенная platform выполнила его, какой digest получен, какой subject attestation совпадает с этим digest и какой digest использовал deploy. Отдельно должна быть запись о границе секрета: где он был разрешён, какие места проверены и какой credential остался действующим.</p>\n<p>Этот критерий не означает, что image безопасен во всех смыслах. Он лишь делает происхождение и секретный риск проверяемыми. Сканирование уязвимостей, лицензий, зависимостей, прав registry и runtime policy остаётся отдельными контролями. Если не хватает одного поля, честный результат — <code>manual review required</code>, а не зелёная галочка по совпадению тега.</p>\n<h2>Ограничения применимости</h2>\n<p>Пример рассчитан на Linux-контейнер и BuildKit с поддержкой secret mounts. Legacy builder, Windows-контейнеры, другой CI или multi-platform registry могут иметь иной синтаксис и другую семантику кеша. Команды с доменом <code>example.invalid</code> — шаблон: они не обращаются к реальному registry и не дают production-результата.</p>\n<p>Image history — полезный источник следов, но не доказательство чистоты: значение могло попасть в кеш, артефакт, рабочий каталог runner или внешний сервис. Provenance — утверждение, которое нужно проверять с выбранными корнями доверия и policy. Подпись гарантирует целостность подписанного объекта в рамках модели доверия, но не безопасность исходного кода и не факт его deploy.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://docs.docker.com/build/building/secrets/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Build secrets</a> — secret mounts, SSH mounts и причина не передавать секреты через build arguments.</li><li><a href=\"https://docs.docker.com/reference/cli/docker/image/history/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: docker image history</a> — просмотр истории инструкций образа и ограничение такого просмотра.</li><li><a href=\"https://slsa.dev/spec/v1.0/provenance\" target=\"_blank\" rel=\"noopener noreferrer\">SLSA v1.0: Provenance</a> — subject, builder, build definition, параметры и правила проверки.</li><li><a href=\"https://docs.sigstore.dev/cosign/verifying/verify/\" target=\"_blank\" rel=\"noopener noreferrer\">Sigstore Cosign: Verifying signatures</a> — проверка image signature и attestation.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218 SSDF</a> — практики безопасной разработки и снижения риска повторения дефекта.</li></ul>"
|
||
}
|