Верификация provenance и политики-гейты
В L01 ты подписал образы и сгенерировал SLSA provenance — но подпись, которую никто не проверяет, ничего не защищает. Половина про принуждение: проверяй подпись + provenance + identity на границе деплоя через admission controller, fail closed, как policy-as-code.
В CI команда сделала всё правильно. Каждый образ подписан cosign, каждая сборка выдавала SLSA provenance attestation, зелёные галочки были прекрасны. Руководству показали слайд «supply chain: защищён». А потом, во время пятничного пожара, инженеру нужно было срочно выкатить хотфикс — он собрал образ на ноутбуке и запустил kubectl set image прямо на прод-кластер. Образ поехал. Ни подписи, ни provenance, ни слова возражения — потому что в кластере ничто никогда не проверяло. Подписи были декорацией. Весь пайплайн, производивший криптографическое доказательство происхождения, вешал это доказательство на стену, пока дверь стояла открытой. Этот урок — недостающая половина: не как подписывать, а как заставить кластер отказываться от всего, что не подписано и не provenance-нуто билдером, которому ты доверяешь, — и отказываться даже тогда, когда сама верификация ломается.
Подпись — это утверждение; защита — это верификация
L01 дал тебе половину «производства»: cosign sign прикрепил подпись к digest образа, а билд-платформа выдала SLSA provenance attestation, описывающий, что его собрало. Это настоящая криптография. И сама по себе она полностью инертна. Подпись — это всего лишь утверждение, привинченное к артефакту, — она ничего не меняет в том, что запускается. Защита возникает только в тот момент, когда что-то отказывается запускать артефакт, не подписанный и не attestation-нутый доверенным билдером. Если ничто не отказывается, подписи — настенное украшение.
Так что вопрос, на который отвечает этот урок, — не «как мне подписывать» (ты это сделал), а «что именно я верифицирую и где живёт отказ».
Что ты верифицируешь: подпись, provenance и identity — по digest
Верификация — это три проверки, и пропуск любой из них оставляет дыру:
# 1. ПОДПИСЬ — этот конкретный digest образа подписан identity, которому я доверяю?
# Keyless: OIDC identity подписавшего записан в Rekor (transparency log),
# а сертификат цепляется к Fulcio. Ты пинишь КОГО, а не долгоживущий ключ.
cosign verify \
--certificate-identity "https://github.com/acme/app/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/app@sha256:abc123...
# 2. PROVENANCE — говорит ли SLSA attestation, что МОЙ билдер собрал это из МОЕГО коммита?
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity "https://github.com/acme/app/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/app@sha256:abc123...Первая проверка отвечает на «это подписано кем-то, кому я доверяю?» Вторая отвечает на более сильный вопрос: «был ли этот образ собран моим CI, из моего репозитория, на коммите, который я могу назвать?» — вот почему provenance важен даже для «подписанного» образа. Атакующий, укравший ключ подписи, может подписать вредоносный образ; но ему нелегко подделать provenance attestation, утверждающий, что твоя билд-платформа собрала его из твоего источника. И каждая проверка прибита к digest образа (@sha256:...), никогда не к меняемому тегу — тег можно перенаправить на другой образ после того, как ты его проверил; digest — это содержимое.
Третья проверка: политика на attestation
Проверить, что подпись валидна, недостаточно — ты должен утвердить, что содержимое совпадает с твоими ожиданиями. Policy-as-code (политика как код — правила безопасности, версионируемые в репозитории и применяемые автоматически) выражает это так, что оно под контролем версий, тестируемо и применяется единообразно:
# Набросок OPA/Rego: впускаем, только если builder == наш CI И source repo == наш
package supplychain
default allow = false
allow {
input.provenance.builder.id == "https://github.com/acme/app/.github/workflows/release.yml@refs/heads/main"
input.provenance.invocation.configSource.uri == "git+https://github.com/acme/app@refs/heads/main"
input.image.digest == input.requested.digest
}Это разница между «существует валидная подпись» и «валидная подпись от моего билдера для моего источника на этом конкретном digest». Утёкший ключ, форк, пересборка из подделанной ветки — все они дают подписи, которые криптографически валидны и отвергнуты политикой.
Где живёт отказ: admission control
Ты мог бы запускать cosign verify в своём CD-джобе деплоя. Это лучше, чем ничего, и это слабый гейт: любой, кто деплоит в обход CI — оператор с kubectl, скомпрометированный CD-шаг, break-glass-скрипт — обходит его полностью. Это ровно инцидент с пятничным хотфиксом. CI-проверка защищает путь, проходящий через CI, а это не тот путь, которым ходят атакующие и паникующие инженеры.
Принуждение, которое ничто не обходит, живёт на рантайм-границе: Kubernetes admission controller (контроллер допуска — плагин API-сервера, перехватывающий каждый запрос на создание ресурса и способный его отклонить). На каждом создании Pod контроллер (Sigstore policy-controller, Kyverno или OPA Gatekeeper) перехватывает запрос, резолвит образ в его digest, верифицирует подпись + provenance против твоей политики и отказывает в admission, если проверка не прошла. Нет «задеплоить в обход» — единственный способ запустить Pod проходит через API-сервер, а API-сервер вызывает webhook.
# Sigstore policy-controller: запускаться могут только подписанные + provenance-нутые
# образы из нашего CI
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-acme-provenance
spec:
images:
- glob: "ghcr.io/acme/**"
authorities:
- keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/acme/app/.github/workflows/release.yml@refs/heads/main"
attestations:
- name: must-have-slsa-provenance
predicateType: "https://slsa.dev/provenance/v1"
# КРИТИЧНО: отказывай, когда верификация не может завершиться, не впускай по ошибке
policy:
fetchConfigFile: trueУровни SLSA: планка, против которой ты верифицируешь
SLSA — это фреймворк, определяющий, насколько сильна гарантия provenance. Более высокие уровни означают, что provenance сгенерирован самой билд-платформой (а не билд-скриптом), сборки изолированы и неподделываемы, а путь от источника к артефакту устойчив к подмене. Ты выбираешь целевой уровень — скажем, «все прод-образы должны соответствовать SLSA Build L3» — и твоя политика верифицирует, что входящие артефакты несут provenance, отвечающий этой планке, прежде чем они деплоятся. Уровень — это контракт; admission control — это то, что его принуждает.
Fail-closed против fail-open: компромисс, решающий всё
Вот тонкость, которая повернула вторую половину этой истории. Проверка верификации может не завершиться: реестр недоступен, transparency log на миг недостижим, attestation отсутствует. Политика должна решить, что делать тогда, и ответов всего два:
- Fail closed — если верификация не может завершиться, отказывай. Артефакт не запускается.
- Fail open — если верификация не может завершиться, впускай всё равно. Артефакт запускается непроверенным.
Fail-open-политика — это молчаливое отсутствие защиты: каждый временный сбой инфраструктуры подписи или лога становится окном, в которое проезжает что угодно. Команда из истории сначала выкатила fail-open-политику; когда Rekor стал на миг недоступен, непроверенные образы проехали прямиком, и никто не заметил, потому что не было отказа, на который сработал бы алерт. Fail closed — единственный корректный дефолт.
Цена реальна, и ты проектируешь под неё, а не вокруг неё: fail-closed означает, что сбой реестра или лога может блокировать деплои. Доступность ты выкупаешь обратно, кешируя trust roots, мониторя путь верификации как зависимость первого класса и относясь к инфраструктуре подписи как к production-критичной, — а не ослабляя политику до fail-open, что меняет проблему доступности деплоя на отсутствие защиты как фичи.
▸Почему это работает
Почему подпись в CI бесполезна без admission-политики, которая верифицирует на деплое? Потому что подпись — это лишь утверждение, прикреплённое к артефакту; она ничего не меняет в том, что запускается, пока верификатор активно не откажется от неподписанных или недоверенных артефактов. Если единственная проверка опциональна или живёт на стороне CI, то любой, кто деплоит в обход CI — оператор с kubectl, скомпрометированный CD-джоб, break-glass-хотфикс — выкатывает непроверенный код, и на подписи никто никогда не смотрит. Принуждение обязано жить на рантайм-границе admission, где гейтится сам акт запуска артефакта, а не акт его сборки. И оно должно fail closed: если оно впускает при ошибке верификации, каждый сбой инфраструктуры подписи молча снова открывает дверь, которую должно было запереть.
Ты должен гарантировать, что в кластере могут запускаться только доверенные, provenance-нутые образы — даже против оператора, деплоящего в обход CI, или скомпрометированного CD-шага. Какой контроль это даёт?
Твой CI подписывает каждый образ и выдаёт SLSA provenance, и все сборки зелёные. Почему это само по себе не защищает прод?
Неподписанный образ доехал до прода, хотя CI подписывал всё. Какое принуждение отсутствовало и какого режима сбоя это принуждение обязано конкретно избегать?
- 01L01 уже подписывает образы и выдаёт SLSA provenance. Объясни точно, почему это само по себе не защищает прод, какие три вещи ты обязан верифицировать и где должна жить верификация.
- 02Пройди admission control как точку принуждения, роль уровней SLSA и почему политика обязана fail closed, а не fail open.
L01 произвёл криптографическое доказательство происхождения — подписи cosign и SLSA provenance — но доказательство, которое никто не проверяет, ничего не защищает; этот урок закрывает петлю, верифицируя и принуждая на границе деплоя. Подпись — лишь утверждение, привинченное к артефакту: защита существует ровно тогда, когда что-то отказывается запускать артефакт, не подписанный и не provenance-нутый доверенным билдером. Верификация — три проверки, все прибитые к неизменяемому digest образа: подпись от доверенного identity (keyless через Fulcio, записано в transparency log Rekor); SLSA provenance attestation говорит, что твой билдер собрал это из твоего коммита источника (так что украденный ключ подписи, который может подписать вред, всё равно падает, потому что не может подделать provenance твоей билд-платформы); и правило policy-as-code утверждает builder id == твой CI, source repo == твой, digest совпадает. Уровни SLSA — это планка (более высокие уровни означают сгенерированные платформой, изолированные, неподделываемые сборки), и ты верифицируешь, что входящие артефакты соответствуют твоему целевому уровню, до деплоя. Отказ должен жить на рантайм-границе admission: Kubernetes admission controller (policy-controller, Kyverno, Gatekeeper) перехватывает каждое создание Pod и отказывает при сбое, бэкстоп, который ничто не обходит, — тогда как CI-проверка cosign verify охраняет только CI-путь и обходится оператором с kubectl, скомпрометированным CD-шагом или break-glass-хотфиксом (история: неподписанная сборка с ноутбука поехала в прод, потому что ничто не проверяло, а затем была отказана, как только появилась admission-политика). Наконец, политика обязана fail CLOSED: если верификация не может завершиться из-за недоступного реестра или transparency log, она отказывает, а не впускает, потому что fail-open это молчаливое отсутствие защиты — каждый сбой инфраструктуры подписи становится окном, в которое выкатываются непроверенные образы (второй баг истории, краткий сбой Rekor). Доступность ты выкупаешь обратно кешированием trust roots и мониторингом пути верификации, никогда не ослаблением до fail-open. Теперь, когда видишь слайд «все образы подписаны — supply chain защищён», задай вопрос: что именно отказывает неподписанному или непровенанс-нутому образу на границе кластера — и делает ли это fail closed?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.