Admission webhook проверяет только тот запрос, который дошёл до API server. Статический Pod создаёт kubelet, прямой доступ к узлу обходит обычный admission-контур, а сбой webhook заставляет выбирать между остановкой кластера и fail-open. Supply Chain NRI Plugin переносит вторую проверку ниже: containerd или CRI-O синхронно спрашивает плагин перед CreateContainer, можно ли запускать конкретный digest.

30 июля инженер Red Hat Саша Грунерт описал этот подход в блоге CNCF. Материал Linux Foundation доступен по CC BY 3.0; эта русская адаптация сокращает исходный текст, добавляет эксплуатационный чек-лист и сверяет команды с последним стабильным релизом плагина.

Почему одного admission webhook мало

Kyverno, OPA Gatekeeper и Sigstore Policy Controller хорошо работают на уровне Kubernetes API: перехватывают создание Pod, проверяют подписи или attestations и разрешают либо отклоняют запрос. Этот слой удобен для ранней обратной связи и кластерных политик, но он видит не все пути к запущенному контейнеру.

Статические Pod управляются kubelet напрямую. API server получает лишь mirror Pod; удаление или отклонение зеркала не останавливает исходный workload на узле. Ошибка в namespace selector тоже способна исключить часть Pod из проверки. Наконец, fail-closed webhook добавляет зависимость от сети и доступности самого policy controller в критический путь создания Pod.

Отсюда не следует, что admission нужно убрать. Он остаётся первым слоем: быстро отклоняет неверный manifest, проверяет labels, security context и другие свойства объекта Kubernetes. Runtime-проверка решает другую задачу: подтверждает, что образ, который фактически собирается выполнить узел, соответствует политике цепочки поставки.

Где NRI ставит контрольную точку

Node Resource Interface — протокол расширения OCI-runtime. Долгоживущий плагин регистрируется в containerd или CRI-O, подписывается на события жизненного цикла и получает синхронный вызов при создании контейнера.

Supply Chain NRI Plugin обрабатывает этот вызов в четыре шага:

  1. извлекает image reference и digest из runtime-аннотаций;
  2. выбирает policy для namespace;
  3. получает attestations из OCI registry и проверяет их;
  4. возвращает разрешение или ошибку до запуска контейнера.

Проверка относится к созданию контейнера, а не к загрузке образа. Поэтому cached и заранее скачанные images проходят тот же gate. API-пользователь не может снять этот контроль label или другим вариантом Pod spec: вызов делает сам runtime.

Граница угроз важна. Плагин доверяет целостности узла. Root на node может остановить процесс, заменить policy-файлы или отключить NRI в конфигурации runtime. Это защита от обхода API-слоя, а не от полностью скомпрометированного хоста.

Что именно проверяется

Плагин понимает три типа attestations. Они отвечают на разные вопросы и не заменяют друг друга.

SLSA provenance: кто и как собрал образ

Проверка подтверждает подпись provenance, builder identity, исходный репозиторий и build type. Политика может разрешить только конкретный GitHub Actions workflow и репозитории организации. Наличие подписи само по себе недостаточно: важна привязка к ожидаемому источнику и сборщику.

VEX: уязвимость найдена, но применима ли она

VEX сообщает состояние CVE: not_affected, under_investigation, fixed или affected. Для нескольких документов выбирается наиболее строгий результат. В enforce-режиме статус affected блокирует контейнер; политика отдельно задаёт поведение при отсутствии VEX.

VSA: проверку уже выполнил доверенный сервис

Verification Summary Attestation хранит итог внешнего verifier. Корректная VSA от разрешённого источника позволяет не повторять SLSA- и VEX-проверки на каждом node. Для большого кластера это уменьшает число обращений к registry и криптографических операций, но переносит доверие на сервис, который подписал summary.

Плагин ищет attestations через OCI Referrers API с fallback на cosign tags. Keyless-подписи проверяются через Sigstore/Fulcio, key-based сценарии тоже поддерживаются.

Политика и эксплуатационные настройки разделены

Операционный TOML управляет режимом, таймаутами, cache и поведением при недоступном registry:

verification = "warn"
fetch_timeout = "30s"
fetch_failure_policy = "warn"
cache_ttl = "24h"
cache_failure_ttl = "5m"
policy_dir = "/etc/nri-supply-chain/policies"

JSON-файлы в policy_dir описывают trust: допустимые OIDC issuers, SAN patterns, source repositories и обязательность SLSA/VEX. default.json действует для всех namespace, а production.json может ужесточить только production.

Разделение полезно организационно. Platform team меняет timeout и cache TTL по характеристикам инфраструктуры, security team — trust roots и реакцию на отсутствующие attestations. При этом policy reload должен оставаться контролируемым изменением: ошибочная пустая policy способна фактически разрешить всё.

Сначала проверяйте документацию релиза, а не ветки main

На 3 августа 2026 года последний опубликованный релиз — v0.1.5. Для него одиночная проверка запускается так:

nri-supply-chain --config config.toml \
  --verify-image ghcr.io/myorg/myimage:v1.0

В README ветки main уже показан интерфейс следующей версии с подкомандой verify и примером 0.2.0, которой пока нет среди GitHub Releases. Это нормальный дрейф между разработкой и stable, но опасный источник сломанных runbook. Фиксируйте ссылку на tag вместе с версией binary и не копируйте команды из main в production-инструкции.

Документация v0.1.5 заявляет Kubernetes 1.26+, CRI-O 1.28+, containerd 1.7+ и NRI 0.6+. NRI должен быть включён в runtime; наличие DaemonSet без рабочего NRI-соединения не создаёт контрольной точки. Upstream-репозиторий NRI при этом прямо помечает проект как DRAFT, поэтому совместимость следует доказывать на конкретных версиях runtime и node image, а не считать стабильной абстракцией по умолчанию.

Безопасный rollout: observe, tighten, enforce

Включать блокировку на всех узлах первым изменением опасно. Практичный rollout состоит из трёх фаз.

1. Observe

Запустите verification = "warn" и разрешите отсутствующие attestations. Плагин проверяет containers, но не мешает старту. Снимите baseline по nri_supply_chain_verification_total, skipped verification, fetch errors, cache hit rate и latency. Отдельно найдите системные images, pause containers и внутренние registry, которым нужна собственная политика.

2. Tighten по namespace

Сначала потребуйте SLSA provenance в одном некритичном namespace, затем в production. Оставьте VEX permissive, если pipeline ещё не выпускает эти документы. Такой порядок превращает отсутствие attestations из аварии в явный backlog для build platform.

3. Enforce

Переводите кластер в enforce только после проверки рестарта node, registry outage и cold cache. Pod event должен содержать понятную причину отказа, а дежурный — знать, какой policy-файл и attestation вызвали решение.

Главный trade-off — registry outage

В стабильной v0.1.5 fetch_failure_policy = "warn" означает fail-open: недоступность registry не останавливает новые containers. deny закрывает обход, но outage registry блокирует запуск ранее не проверенного image. Cache помогает только для уже известных сочетаний digest и namespace.

Нужны два отдельных решения, а не одно слово «безопасность»:

  • что делать при невалидной attestation — обычно deny;
  • что делать, когда attestation невозможно получить — зависит от availability SLO и threat model.

Перед enforce проверьте circuit breaker, TTL успешных и ошибочных результатов, доступ к private registry, ротацию Sigstore roots и поведение после перезапуска. Алерты нужны не только на rejected containers, но и на missing_annotations, circuit breaker trips и рост p99 verification latency.

Когда runtime-проверка оправдана

NRI-слой полезен, если threat model включает static pods, прямой kubelet/node access, ошибочную конфигурацию webhook или требование проверять каждый реально создаваемый container. Для небольшого кластера без attestations в CI он добавит сложность раньше пользы: сначала наладьте provenance, identity и policy ownership.

Правильная архитектура здесь двухслойная. Admission отвечает: «разрешён ли этот Kubernetes-объект?» Runtime отвечает: «разрешено ли выполнить этот digest на этом node?» Первый слой даёт раннюю обратную связь, второй проверяет последнее действие перед стартом процесса. Вместе они закрывают разные пути, а не дублируют одну и ту же проверку.