{"index":164,"slug":"editorial-2023-06-mechanism-secrets-supply-chain","title":"Attestation не доказывает provenance: как связать секрет, сборку и артефакт","excerpt":"Файл attestation рядом с образом ещё не подтверждает его происхождение. Разбираем границы секрета, digest, builder identity и проверку, которая связывает provenance с тем, что действительно попадает в deploy.","contentHtml":"
Симптом появляется после сборки: в registry лежат образ и attestation. В ней видны revision и builder, поэтому релиз помечают как проверенный. Но statement может ссылаться на другой digest, identity сборщика может не входить в доверенную политику, а deploy может получить образ по тегу latest. В этом случае команда не знает, какой именно набор байтов запущен, и не может уверенно выбрать rollback.
Секрет добавляет ещё один разрыв. Токен, доступный CI, не становится безопасным только потому, что его нет в финальном файле: он мог попасть в командную строку, лог, cache или промежуточный слой. Provenance отвечает на вопрос «как получен output», а secret boundary — «где чувствительное значение могло быть доступно». Это разные утверждения и разные проверки.
\nПрактический критерий такой: attestation — это evidence только после проверки подписи и доверенной identity, сопоставления subject с digest образа, проверки ожидаемых claims и связи того же digest с входом deploy. Если одного звена нет, корректный статус — «не проверено», а не «безопасно».
В цепочке поставки полезно хранить не один большой флаг, а несколько наблюдаемых полей. Source revision — неизменяемая ревизия исходного кода. Builder identity — идентификатор платформы или workflow, которому доверяют выпуск результата. Artifact digest — криптографический отпечаток конкретных байтов. Attestation statement — подписанное утверждение о subject и свойствах сборки. Deploy input — digest, который фактически передал релизный механизм.
\nСекретная граница описывается отдельно: какая job получает ссылку или значение, на каком шаге, в каком виде и где оно может оказаться после шага. Не следует выводить один факт из соседнего. Digest не говорит, кто собрал образ. Revision не доказывает, что сборщик использовал её. Наличие подписи не доказывает, что signer разрешён именно для этого репозитория.
\n| Факт | Допустимое утверждение | Что ещё не доказано | Проверка |
|---|---|---|---|
| Source | Входом названа ревизия abc123. | Эта ревизия действительно попала в output. | Сверить source в build invocation и provenance. |
| Builder | В statement указан builder.id. | Identity входит в доверенный список. | Сопоставить signer и builder с trust policy. |
| Artifact | Известен digest образа. | Именно он ушёл в deploy. | Прочитать immutable release input. |
| Attestation | Есть statement с subject. | Подпись, claims и формат проверены. | Запустить verifier с заданными ожиданиями. |
| Secret boundary | Шаг получает секрет через временный mount. | Значение не сохранилось в output и журналах. | Проверить Dockerfile, логи, cache и слои выбранного digest. |
В attestation subject идентифицирует artifact, к которому относится statement. Для контейнерного образа таким идентификатором обычно служит digest манифеста, а не подвижный тег. Тег удобен человеку, но может быть перепривязан к другой версии. Поэтому release record должен сохранять запись вроде registry.example/api@sha256:... и передавать в deploy именно её.
Расхождение subject и deploy input — достаточная причина остановить автоматический выпуск. Даже если revision и builder выглядят знакомо, statement про образ B ничего не доказывает для образа A. Сверка должна быть буквальной: алгоритм и полное значение digest должны совпадать после нормализации формата, принятой вашим registry.
\nСледующий уровень — ожидания к provenance. Помимо подписи и builder.id, проверяют канонический репозиторий, buildType и внешние параметры сборки. Если verifier принимает неизвестные параметры молча, атакующий или ошибочная конфигурация могут изменить результат при сохранении внешне правдоподобного statement.
Секрет — не только значение в переменной окружения. Это ещё аргументы процесса, файлы в рабочем каталоге, вывод команды, cache и слои образа. Docker прямо предупреждает, что build arguments и environment variables сохраняются в финальном образе, и предлагает secret mounts или SSH mounts для временного доступа.
\nБезопаснее ограничить секрет одним шагом BuildKit. Например, Dockerfile может прочитать credential из стандартного пути mount и использовать его для получения зависимости:
\n# 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 > /dev/null && rm /tmp/dependency.tar.gz\nКоманда сборки передаёт секрет через клиент Docker, а не через ARG:
DOCKER_BUILDKIT=1 docker build --secret id=private_token,env=PRIVATE_TOKEN -t registry.example/api:build-42 .\nЭто пример границы доступа, а не сертификат отсутствия утечки. Если команда записала token в другой файл, shell включил трассировку или зависимость вывела заголовок в stdout, mount сам по себе не исправит проблему. После сборки нужно проверять именно выбранный digest и места, где он мог оставить данные.
\nСначала полезно проверить отрицательный путь на локальной фикстуре. Фрагмент ниже не имитирует криптографическую подпись и не обращается к registry. Он проверяет только две связи: statement относится к тому же digest, который попал в release record, и builder разрешён политикой. Запустите его как есть командой node --input-type=module:
node --input-type=module <<'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\nОжидаемый результат — ошибка о ручной проверке: subject намеренно указывает на artifact-b. Если заменить его на sha256:artifact-a, фикстура напечатает fixture passed. Это воспроизводит контракт сравнения, но не подменяет verifier: здесь нет проверки envelope, сертификата, transparency log, source revision и реального deploy.
Для Cosign официальный сценарий начинается с образа, к которому attestation уже прикреплена. Команда ниже проверяет attestation с публичным ключом; значения URI и файла — проектные, поэтому их нужно заменить на доверенные для вашей системы:
\nIMAGE=registry.example.com/api@sha256:REPLACE_WITH_RELEASE_DIGEST\ncosign verify-attestation --key cosign.pub \"$IMAGE\"\nУспешный exit code означает, что выбранный режим Cosign проверил подпись по указанному ключу и нашёл attestation для этого образа. Он не отвечает за вашу бизнес-политику автоматически. В policy нужно дополнительно зафиксировать допустимый builder.id, репозиторий, buildType, revision и способ получения deploy input. При keyless-проверке вместо файла ключа задают ожидаемые certificate identity и OIDC issuer; их нельзя оставлять широкими регулярными выражениями без причины.
Практический evidence-пакет не должен содержать секреты. Достаточно сохранить digest, идентификатор statement, signer или certificate identity, результат verifier, параметры policy, время и ссылку на release record. Само наличие вывода в терминале не заменяет хранения результата там, где его сможет проверить следующий участник.
\n| Симптом | Вероятная причина | Точная проверка | Решение |
|---|---|---|---|
| Attestation есть, deploy использует тег. | Release не зафиксировал immutable input. | Прочитать digest из фактической конфигурации deploy. | Остановить автоматический выпуск и связать release с digest. |
| Subject не равен digest registry. | Statement относится к другому output или перепутан. | Сравнить subject, manifest digest и deploy input. | Отклонить statement и запросить новое evidence. |
| Builder указан, signer не проверен. | Identity приняли за доверие. | Проверить signer, цепочку сертификатов и trust policy. | Оставить статус not-verified. |
| Секрет был доступен build, слои не исследованы. | Границу доступа описали, но не проверили output. | Проверить Dockerfile, history, cache, logs и выбранные слои. | Не публиковать значение; при сомнении ротировать секрет. |
| Известна revision, но нет записи invocation. | Источник назван задним числом. | Найти provenance с входами конкретной сборки. | Не утверждать, что output собран из этой revision. |
| Нужен rollback, а digest неизвестен. | Релиз хранит только тег. | Сверить registry history, release record и runtime image ID. | Сначала установить безопасный кандидат, затем откатывать. |
builder.id и certificate identity соответствуют policy.buildType и внешние параметры с ожидаемыми значениями.Provenance описывает происхождение output в пределах модели и доверия к builder. Она не доказывает отсутствие уязвимостей, доброкачественность исходного кода, безопасность зависимостей или отсутствие вредоносного инсайдера на самой build-платформе. SLSA отдельно указывает, что доверие к build platform остаётся частью модели verifier.
\nПодпись защищает целостность statement относительно ключа или identity. Она не делает claims истинными без проверки контекста. Digest связывает конкретные байты, но не отвечает, разрешено ли запускать их в production. Secret mount уменьшает риск сохранения значения в слое, но не защищает от утечки через команду, зависимость, лог или неправильную очистку.
\nУчебные команды используют фиктивные URI и digest. Первая команда запускается локально, вторая потребует установленного Cosign, доступного registry, реального attestation и доверенного ключа или keyless-политики. Они не проверяют конкретный production pipeline и не дают права объявлять его безопасным без чтения фактических записей.
\nРешение о deploy можно принимать, когда одна цепочка записей связывает revision, builder identity, statement subject, artifact digest и deploy input; verifier подтвердил подпись; policy одобрила builder, repository, buildType и claims; а secret boundary проверена для выбранного output. Любое расхождение переводит релиз в ручной review с понятным владельцем следующего действия.
buildType и external parameters.cosign verify и cosign verify-attestation, включая проверку claims.