Files
progcode/editorial/agent-rewrites/164.json
T

2 lines
21 KiB
JSON
Raw 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":164,"slug":"editorial-2023-06-mechanism-secrets-supply-chain","title":"Attestation не доказывает provenance: как связать секрет, сборку и артефакт","excerpt":"Файл attestation рядом с образом ещё не подтверждает его происхождение. Разбираем границы секрета, digest, builder identity и проверку, которая связывает provenance с тем, что действительно попадает в deploy.","contentHtml":"<p>Симптом появляется после сборки: в registry лежат образ и attestation. В ней видны revision и builder, поэтому релиз помечают как проверенный. Но statement может ссылаться на другой digest, identity сборщика может не входить в доверенную политику, а deploy может получить образ по тегу <code>latest</code>. В этом случае команда не знает, какой именно набор байтов запущен, и не может уверенно выбрать rollback.</p>\n<p>Секрет добавляет ещё один разрыв. Токен, доступный CI, не становится безопасным только потому, что его нет в финальном файле: он мог попасть в командную строку, лог, cache или промежуточный слой. Provenance отвечает на вопрос «как получен output», а secret boundary — «где чувствительное значение могло быть доступно». Это разные утверждения и разные проверки.</p>\n<p>Практический критерий такой: attestation — это evidence только после проверки подписи и доверенной identity, сопоставления <code>subject</code> с digest образа, проверки ожидаемых claims и связи того же digest с входом deploy. Если одного звена нет, корректный статус — «не проверено», а не «безопасно».</p>\n<h2>Сначала разделим факты цепочки</h2>\n<p>В цепочке поставки полезно хранить не один большой флаг, а несколько наблюдаемых полей. <strong>Source revision</strong> — неизменяемая ревизия исходного кода. <strong>Builder identity</strong> — идентификатор платформы или workflow, которому доверяют выпуск результата. <strong>Artifact digest</strong> — криптографический отпечаток конкретных байтов. <strong>Attestation statement</strong> — подписанное утверждение о subject и свойствах сборки. <strong>Deploy input</strong> — digest, который фактически передал релизный механизм.</p>\n<p>Секретная граница описывается отдельно: какая job получает ссылку или значение, на каком шаге, в каком виде и где оно может оказаться после шага. Не следует выводить один факт из соседнего. Digest не говорит, кто собрал образ. Revision не доказывает, что сборщик использовал её. Наличие подписи не доказывает, что signer разрешён именно для этого репозитория.</p>\n<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>Source</td><td>Входом названа ревизия <code>abc123</code>.</td><td>Эта ревизия действительно попала в output.</td><td>Сверить source в build invocation и provenance.</td></tr><tr><td>Builder</td><td>В statement указан <code>builder.id</code>.</td><td>Identity входит в доверенный список.</td><td>Сопоставить signer и builder с trust policy.</td></tr><tr><td>Artifact</td><td>Известен digest образа.</td><td>Именно он ушёл в deploy.</td><td>Прочитать immutable release input.</td></tr><tr><td>Attestation</td><td>Есть statement с subject.</td><td>Подпись, claims и формат проверены.</td><td>Запустить verifier с заданными ожиданиями.</td></tr><tr><td>Secret boundary</td><td>Шаг получает секрет через временный mount.</td><td>Значение не сохранилось в output и журналах.</td><td>Проверить Dockerfile, логи, cache и слои выбранного digest.</td></tr></tbody></table>\n<figure><img src=\"/assets/editorial/2023/secrets-supply-chain-2023-attestation-contract.svg\" alt=\"Схема связи source revision, builder identity, secret boundary и artifact digest с attestation и отдельной проверкой перед deploy\" loading=\"lazy\" /><figcaption>Схема показывает границу между данными декларации и результатом verification. Это учебная иллюстрация, а не журнал CI и не доказательство для конкретного образа.</figcaption></figure>\n<h2>Почему subject и digest должны совпасть</h2>\n<p>В attestation subject идентифицирует artifact, к которому относится statement. Для контейнерного образа таким идентификатором обычно служит digest манифеста, а не подвижный тег. Тег удобен человеку, но может быть перепривязан к другой версии. Поэтому release record должен сохранять запись вроде <code>registry.example/api@sha256:...</code> и передавать в deploy именно её.</p>\n<p>Расхождение subject и deploy input — достаточная причина остановить автоматический выпуск. Даже если revision и builder выглядят знакомо, statement про образ B ничего не доказывает для образа A. Сверка должна быть буквальной: алгоритм и полное значение digest должны совпадать после нормализации формата, принятой вашим registry.</p>\n<p>Следующий уровень — ожидания к provenance. Помимо подписи и <code>builder.id</code>, проверяют канонический репозиторий, <code>buildType</code> и внешние параметры сборки. Если verifier принимает неизвестные параметры молча, атакующий или ошибочная конфигурация могут изменить результат при сохранении внешне правдоподобного statement.</p>\n<h2>Секретная граница проходит через весь build</h2>\n<p>Секрет — не только значение в переменной окружения. Это ещё аргументы процесса, файлы в рабочем каталоге, вывод команды, cache и слои образа. Docker прямо предупреждает, что build arguments и environment variables сохраняются в финальном образе, и предлагает secret mounts или SSH mounts для временного доступа.</p>\n<p>Безопаснее ограничить секрет одним шагом BuildKit. Например, Dockerfile может прочитать credential из стандартного пути mount и использовать его для получения зависимости:</p>\n<pre><code># syntax=docker/dockerfile:1\nFROM alpine:3.20\nRUN --mount=type=secret,id=private_token,target=/run/secrets/private_token wget --header=\"Authorization: Bearer $(cat /run/secrets/private_token)\" -O /tmp/dependency.tar.gz https://packages.example.invalid/dependency.tar.gz\nRUN tar -tf /tmp/dependency.tar.gz &gt; /dev/null &amp;&amp; rm /tmp/dependency.tar.gz</code></pre>\n<p>Команда сборки передаёт секрет через клиент Docker, а не через <code>ARG</code>:</p>\n<pre><code>DOCKER_BUILDKIT=1 docker build --secret id=private_token,env=PRIVATE_TOKEN -t registry.example/api:build-42 .</code></pre>\n<p>Это пример границы доступа, а не сертификат отсутствия утечки. Если команда записала token в другой файл, shell включил трассировку или зависимость вывела заголовок в stdout, mount сам по себе не исправит проблему. После сборки нужно проверять именно выбранный digest и места, где он мог оставить данные.</p>\n<h2>Минимальная воспроизводимая проверка</h2>\n<p>Сначала полезно проверить отрицательный путь на локальной фикстуре. Фрагмент ниже не имитирует криптографическую подпись и не обращается к registry. Он проверяет только две связи: statement относится к тому же digest, который попал в release record, и builder разрешён политикой. Запустите его как есть командой <code>node --input-type=module</code>:</p>\n<pre><code>node --input-type=module &lt;&lt;'NODE'\nconst release = {\n deployInput: 'sha256:artifact-a',\n expectedBuilder: 'https://ci.example/builders/trusted',\n};\n\nconst statement = {\n subjectDigest: 'sha256:artifact-b',\n builderId: 'https://ci.example/builders/trusted',\n};\n\nconst subjectMatches = statement.subjectDigest === release.deployInput;\nconst builderMatches = statement.builderId === release.expectedBuilder;\n\nif (!subjectMatches || !builderMatches) {\n throw new Error('manual review: provenance does not match release input');\n}\n\nconsole.log('fixture passed');\nNODE</code></pre>\n<p>Ожидаемый результат — ошибка о ручной проверке: subject намеренно указывает на <code>artifact-b</code>. Если заменить его на <code>sha256:artifact-a</code>, фикстура напечатает <code>fixture passed</code>. Это воспроизводит контракт сравнения, но не подменяет verifier: здесь нет проверки envelope, сертификата, transparency log, source revision и реального deploy.</p>\n<h2>Как проверять реальный образ</h2>\n<p>Для Cosign официальный сценарий начинается с образа, к которому attestation уже прикреплена. Команда ниже проверяет attestation с публичным ключом; значения URI и файла — проектные, поэтому их нужно заменить на доверенные для вашей системы:</p>\n<pre><code>IMAGE=registry.example.com/api@sha256:REPLACE_WITH_RELEASE_DIGEST\ncosign verify-attestation --key cosign.pub \"$IMAGE\"</code></pre>\n<p>Успешный exit code означает, что выбранный режим Cosign проверил подпись по указанному ключу и нашёл attestation для этого образа. Он не отвечает за вашу бизнес-политику автоматически. В policy нужно дополнительно зафиксировать допустимый <code>builder.id</code>, репозиторий, <code>buildType</code>, revision и способ получения deploy input. При keyless-проверке вместо файла ключа задают ожидаемые certificate identity и OIDC issuer; их нельзя оставлять широкими регулярными выражениями без причины.</p>\n<p>Практический evidence-пакет не должен содержать секреты. Достаточно сохранить digest, идентификатор statement, signer или certificate identity, результат verifier, параметры policy, время и ссылку на release record. Само наличие вывода в терминале не заменяет хранения результата там, где его сможет проверить следующий участник.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<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>Attestation есть, deploy использует тег.</td><td>Release не зафиксировал immutable input.</td><td>Прочитать digest из фактической конфигурации deploy.</td><td>Остановить автоматический выпуск и связать release с digest.</td></tr><tr><td>Subject не равен digest registry.</td><td>Statement относится к другому output или перепутан.</td><td>Сравнить subject, manifest digest и deploy input.</td><td>Отклонить statement и запросить новое evidence.</td></tr><tr><td>Builder указан, signer не проверен.</td><td>Identity приняли за доверие.</td><td>Проверить signer, цепочку сертификатов и trust policy.</td><td>Оставить статус <code>not-verified</code>.</td></tr><tr><td>Секрет был доступен build, слои не исследованы.</td><td>Границу доступа описали, но не проверили output.</td><td>Проверить Dockerfile, history, cache, logs и выбранные слои.</td><td>Не публиковать значение; при сомнении ротировать секрет.</td></tr><tr><td>Известна revision, но нет записи invocation.</td><td>Источник назван задним числом.</td><td>Найти provenance с входами конкретной сборки.</td><td>Не утверждать, что output собран из этой revision.</td></tr><tr><td>Нужен rollback, а digest неизвестен.</td><td>Релиз хранит только тег.</td><td>Сверить registry history, release record и runtime image ID.</td><td>Сначала установить безопасный кандидат, затем откатывать.</td></tr></tbody></table>\n<h2>Порядок проверки перед deploy</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите, что именно не сходится: тег, subject, builder, signer, revision или путь секрета.</li><li><strong>Назовите объект.</strong> Укажите repository, полный artifact digest, release ID и источник deploy input. Секреты и токены в evidence не копируйте.</li><li><strong>Проверьте subject.</strong> Сопоставьте digest statement с digest manifest и тем, что передаёт релизный механизм.</li><li><strong>Проверьте доверие.</strong> Убедитесь, что подпись валидна, signer разрешён, а <code>builder.id</code> и certificate identity соответствуют policy.</li><li><strong>Проверьте claims.</strong> Сравните канонический репозиторий, revision, <code>buildType</code> и внешние параметры с ожидаемыми значениями.</li><li><strong>Проверьте секретную границу.</strong> Назовите job и шаг, затем исследуйте stdout, аргументы, рабочие файлы, cache и слои именно этого output.</li><li><strong>Проверьте отказ.</strong> Для другого subject, неизвестного builder или отсутствующей записи deploy должен сработать останов или ручной review, а не «best effort».</li><li><strong>Сохраните минимальное evidence.</strong> Зафиксируйте digest, revision, identity, verifier, policy version, время и итог. Чувствительные значения оставьте в secret manager.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Provenance описывает происхождение output в пределах модели и доверия к builder. Она не доказывает отсутствие уязвимостей, доброкачественность исходного кода, безопасность зависимостей или отсутствие вредоносного инсайдера на самой build-платформе. SLSA отдельно указывает, что доверие к build platform остаётся частью модели verifier.</p>\n<p>Подпись защищает целостность statement относительно ключа или identity. Она не делает claims истинными без проверки контекста. Digest связывает конкретные байты, но не отвечает, разрешено ли запускать их в production. Secret mount уменьшает риск сохранения значения в слое, но не защищает от утечки через команду, зависимость, лог или неправильную очистку.</p>\n<p>Учебные команды используют фиктивные URI и digest. Первая команда запускается локально, вторая потребует установленного Cosign, доступного registry, реального attestation и доверенного ключа или keyless-политики. Они не проверяют конкретный production pipeline и не дают права объявлять его безопасным без чтения фактических записей.</p>\n<h2>Критерий готовности</h2>\n<p>Решение о deploy можно принимать, когда одна цепочка записей связывает revision, builder identity, statement subject, artifact digest и deploy input; verifier подтвердил подпись; policy одобрила builder, repository, <code>buildType</code> и claims; а secret boundary проверена для выбранного output. Любое расхождение переводит релиз в ручной review с понятным владельцем следующего действия.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://slsa.dev/spec/v1.2/provenance\" target=\"_blank\" rel=\"noopener noreferrer\">SLSA v1.2: Provenance</a> — определение provenance и связь build output с исходным кодом.</li><li><a href=\"https://slsa.dev/spec/v1.2/verifying-artifacts\" target=\"_blank\" rel=\"noopener noreferrer\">SLSA v1.2: Verifying artifacts</a> — проверка подписи, subject, builder identity, <code>buildType</code> и external parameters.</li><li><a href=\"https://docs.sigstore.dev/cosign/verifying/verify/\" target=\"_blank\" rel=\"noopener noreferrer\">Sigstore Cosign: Verifying signatures</a> — официальные команды <code>cosign verify</code> и <code>cosign verify-attestation</code>, включая проверку claims.</li><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/environment variables.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218 SSDF v1.1</a> — официальный набор практик безопасной разработки и управления риском цепочки поставки.</li></ul>"}