{ "index": 163, "slug": "editorial-2023-06-field-secrets-supply-chain", "title": "Когда deploy виден, а происхождение нет: проверяем цепочку поставки", "excerpt": "Практический разбор разрыва между исходным кодом, сборкой, digest, декларацией и deploy input. Секретная граница, отрицательный путь и критерий, после которого выпуск можно проверять дальше.", "contentHtml": "
В отчёте CI есть успешная сборка. В registry лежит образ. Deploy указывает на digest. Но команда не может ответить, из какого commit собран этот digest, какой builder его выпустил и какая декларация относится именно к нему. Ошибка проявляется поздно: релиз уже обсуждают, а provenance приходится восстанавливать по разным системам.
\nЦена разрыва — не только задержка. Команда может принять чужой образ за результат доверенной сборки. Она может отозвать не тот credential, откатить не тот digest или назвать подписанную декларацию доказательством факта, которого она не проверяет. Секрет при этом способен попасть в логи, слой образа, кеш или артефакт CI.
\nТезис простой: deploy сам по себе показывает только выбранный вход. Без сопоставления source revision → builder identity → secret boundary → artifact digest → declaration → deploy input нельзя утверждать происхождение результата. Каждый переход требует своего evidence. Отсутствующий переход переводит выпуск в ручной review, а не в подтверждённое provenance.
\nЦепочка поставки состоит из разных утверждений. Commit отвечает на вопрос о входном коде. Builder и его identity отвечают на вопрос о процессе сборки. Secret boundary показывает, какие данные могли попасть в процесс и куда им запрещено уходить. Digest связывает байты артефакта с конкретным output. Declaration описывает claims о сборке. Deploy input показывает, что именно пытались применить.
\nСоседнее утверждение не заменяет пропущенное. Имя job не доказывает, что job выполнила сборку. Digest не доказывает commit. Декларация не доказывает, что её subject попал в deploy. Подпись подтверждает целостность подписанного объекта при корректной проверке, но не превращает любой текст в наблюдение среды.
\nТакой разбор нужен и для секретов. Секрет не должен проходить через Dockerfile, командную строку, переменную, которую печатает shell, или общий кеш. Значение может быть скрыто в логе, но остаться в слое образа. Оно может исчезнуть из образа, но сохраниться в артефакте или history. Поэтому проверяют не только содержимое файла, но и границы процесса.
\nНачните с одной карточки выпуска. В ней достаточно шести полей: commit, builder, digest, declaration subject, deploy input и граница секрета. Для каждого поля запишите источник, время получения и допустимый способ просмотра. Не копируйте значение секрета. Нужен факт его отсутствия или контролируемого использования, а не само значение.
\nconst release = {\n sourceRevision: 'abc123',\n builderIdentity: 'ci.example/build-prod',\n artifactDigest: 'sha256:...',\n declarationSubject: 'sha256:...',\n deployInput: 'sha256:...'\n};\n\nconst sameArtifact =\n release.artifactDigest === release.declarationSubject &&\n release.artifactDigest === release.deployInput;\n\nif (!sameArtifact) {\n throw new Error('manual review: artifact links do not match');\n}\n\n// Учебный пример. Он не проверяет подпись, CI, registry или production.\n// Реальные форматы полей и правила доверия задаёт конкретная среда.\n\nКод показывает только одну проверяемую связь: одинаковый digest в артефакте, декларации и deploy input. Он не доказывает, что commit действительно участвовал в сборке. Он не проверяет identity builder, подпись, policy или содержание секрета. Если хотя бы одно поле недоступно, безопасный результат этого примера — ручной review.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Deploy содержит digest, но нет commit | Артефакт отделён от записи сборки | Сопоставьте digest с output конкретной job | Остановите вывод о provenance до появления связи |
| Declaration есть, subject не совпадает | Выбрана декларация другого артефакта | Сравните subject digest и deploy input | Переведите выпуск в manual review |
| Builder указан именем job | Нет проверяемой identity и доверенной границы | Найдите issuer, workflow и policy проверки | Назначьте отдельную проверку builder |
| Секрет исчез из лога, но попал в image history | Значение передали в Dockerfile или командной строке | Проверьте слои, history, cache и export | Удалите секрет из build input и смените credential |
| Есть digest и подпись, но нет deploy link | Подписали объект, не проверив его использование | Сверьте точный digest с manifest deploy | Не называйте подпись доказательством release |
Сборка должна получать секрет только там, где он нужен, и только на время операции. Не задавайте его через ARG, если значение может попасть в историю слоёв. Не выводите окружение командой вроде env в диагностический лог. Не сохраняйте рабочий каталог с credential в артефакт CI. Не передавайте секрет в шаг, который собирает публичный output.
Учебный фрагмент ниже показывает безопасную мысль, а не готовую конфигурацию конкретного CI:
\n# Учебный пример: имя секрета передаётся в действие, значение не печатается.\n# Фактический синтаксис зависит от CI и secret store.\nrun: ./publish.sh\nenv:\n REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}\n\n# publish.sh не выполняет: set -x, env, printenv, cat /proc/*/environ\n# и не складывает каталог с credential в артефакты.\nЭтот пример не доказывает отсутствие утечки. Его нужно дополнить проверкой логов, временных файлов, кеша, image history и прав доступа. Если токен уже появился в публичном output или общем registry, удаление строки из конфигурации не закрывает инцидент. Сначала ограничьте доступ и смените credential по процедуре владельца.
\nDigest полезен потому, что связывает имя output с конкретным содержимым. Поэтому release должен хранить полный digest, а не только tag вроде latest. Tag может указывать на другой объект после публикации. Но digest остаётся только якорем. Он не сообщает, кто собрал образ, с каким исходным кодом и какие входы получил builder.
Проверяйте цепочку в прямом порядке, даже если проблема обнаружилась на deploy. Найдите commit. Найдите запись builder. Получите digest output. Сверьте subject декларации. Сверьте manifest deploy. После этого отдельно проверьте, какие данные видел процесс сборки и где они могли сохраниться. Такой порядок не позволяет начать с красивой декларации и подогнать под неё остальные факты.
\nПроверка должна явно описывать отказ. Если declaration subject отличается от deploy digest, не выбирайте ближайший digest по времени. Если builder не имеет проверяемой identity, не принимайте название workflow за identity. Если secret попал в слой, не ограничивайтесь удалением тега: образ и связанные кеши уже требуют отдельной обработки.
\nНеудача проверки не всегда означает компрометацию. Она означает, что текущих данных недостаточно для заявленного вывода. Это важное различие. Статус not-verified честнее, чем pass, построенный на совпадении имён. Дальнейшее действие выбирают по риску: остановка выпуска, получение evidence, смена credential или rollback к известному digest.
Provenance не заменяет сканирование уязвимостей, контроль доступа, защиту registry и проверку содержания артефакта. Подпись не заменяет проверку subject и trusted identity. Digest не гарантирует безопасный исходный код. Secret store не защищает от вывода значения в лог, если build step печатает окружение.
\nУчебные примеры в статье не выполняют криптографическую проверку, не обращаются к CI, registry или production и не дают production-результатов. Формат declaration, issuer, policy и процедура отзыва зависят от ваших инструментов. Не объявляйте соответствие SLSA или SSDF по одному найденному полю. Сначала проверьте применимый профиль и границы заявленного уровня.
\nRollback тоже имеет границу. Он может вернуть известный deploy input, но не удаляет уже скачанный образ, не отзывает credential и не исправляет запись в чужом кеше. Эти действия требуют отдельной операционной процедуры и владельцев. Если известного кандидата нет, безопаснее остановить выпуск и сохранить минимальное evidence.
\nПроверка готова, если команда показывает одну карточку выпуска и отвечает на пять вопросов: какой commit вошёл в сборку, какой builder выполнил её, какой digest получен, какой declaration subject с ним совпадает и какой digest указан в deploy. Дополнительно команда показывает, где проверена секретная граница и какой отрицательный сценарий остановил выпуск.
\nКритерий не требует утверждать больше, чем доказано. Если любой ответ опирается на имя, tag, текст в ticket или декларацию без независимого сопоставления, статус остаётся not-verified. Если все связи проверены допустимыми evidence, отрицательный путь блокирует несоответствие, а план обработки секрета известен, следующий шаг можно принимать в рамках policy конкретной системы.