Безопасность цепочки поставки и runtime: где остановить риск, а где его увидеть
<p>Контейнерный образ — это не просто файл для запуска. Он несёт код, зависимости, настройки и историю сборки; затем один и тот же артефакт проходит registry, CI/CD, Kubernetes API и runtime узла. Поэтому разговор о «сканировании образов» недостаточен. Нужна цепочка решений: какой артефакт мы допускаем, кто это проверяет до запуска, что ограничивает сам Pod и какие события после запуска требуют реакции.</p> <p>Важная оговорка: Kubernetes предоставляет механизмы для запуска образов, admission control и Pod Security Admission, но не даёт готовой универсальной системы сканирования, SBOM, проверки подписи или runtime detection. Эти части выбираются отдельно. Хорошая программа безопасности описывает границы продукта, а не обещает, что настройка Kubernetes сама собой проверит весь supply chain.</p> <h2>От тега к проверяемому артефакту</h2> <p>Тег образа удобен человеку (<code>payments:1.8</code>), но не является неизменяемой идентичностью: тот же тег может указывать на другой образ. Для развертывания и расследования полезно фиксировать digest — точное содержимое образа. Это даёт ответ на вопрос «какой именно артефакт работал», но само по себе не говорит, кто его собрал и безопасен ли он.</p> <p>Практическая цепочка выглядит так:</p> <ol> <li>CI собирает образ из контролируемого исходного кода и фиксирует версию зависимостей.</li> <li>Сканер анализирует образ и формирует результат по уязвимостям; при необходимости создаётся SBOM.</li> <li>Артефакт попадает в registry, где для него сохраняются digest, результат проверки и, если выбрано решение для provenance, подпись/аттестация.</li> <li>Deployment ссылается на согласованный digest. Admission-проверка сверяет, что объект соответствует минимальным правилам организации.</li> <li>После запуска Pod Security Admission и <code>securityContext</code> ограничивают опасные параметры запуска, а runtime-датчик либо аудит собирают события процесса и сети.</li> </ol> <p>Каждый этап отвечает на свой вопрос. Сканер отвечает «известна ли уязвимость на момент анализа». Подпись или аттестация — «есть ли проверяемое происхождение». Admission — «можно ли создать этот объект сейчас». Runtime-наблюдение — «что процесс делает после старта». Смешивать эти ответы означает оставлять дыры: например, без admission уязвимый или непроверенный образ всё равно может быть создан вручную, а без runtime можно не заметить эксплуатацию уже запущенного процесса.</p> <h2>Сканирование: риск, а не автоматический приговор</h2> <p>У сканера есть ложные совпадения, неполные базы и уязвимости без исправления. Поэтому правильная политика не звучит как «запрещать всё с CVE». Она определяет контекст: критичность, достижимость компонента, наличие фиксированной версии, публичность сервиса и срок исключения. Исключение должно быть ограниченным по времени, иметь владельца и ссылку на риск-решение. Иначе список исключений превращается в постоянный обход защиты.</p> <p>Пример рабочего gate: критическая уязвимость в образе, доступном из Internet, блокирует продвижение до исправления или формально оформленного временного исключения. Средняя уязвимость в неиспользуемой утилите не блокирует релиз автоматически, но попадает в backlog с датой пересмотра. Такой подход сохраняет безопасность и не заставляет команды обходить CI.</p> <p>Проверяйте не только «сканер запустился», а способность восстановить картину: для работающего Deployment можно назвать digest, источник сборки, время сканирования, результат политики и владельца исключения. Если хотя бы одного звена нет, расследование будет опираться на догадки.</p> <h2>Admission — точка, где можно предотвратить запуск</h2> <p>Admission controllers обрабатывают запрос к Kubernetes API до сохранения объекта. Это полезная точка для общих правил: допустимые registries, запрет mutable-тегов, минимальные поля <code>securityContext</code>, проверка подписи выбранным внешним валидатором. Но admission не заменяет CI: он не должен заново выполнять тяжёлый анализ при каждом <code>kubectl apply</code> и не видит дальнейшее поведение процесса.</p> <p>Начните с малой, объяснимой политики. Например: рабочие namespaces принимают образы только из утверждённого registry и требуют digest; исключение возможно только в специально отмеченном namespace с владельцем и сроком. Сначала включите режим аудита или предупреждения, соберите реальные нарушения и исправьте шаблоны Helm/Kustomize. Блокирующий режим включайте по namespace, а не одним изменением для всей production-среды.</p> <p>Не делайте admission-webhook единственной критической точкой без плана доступности. Нужно заранее решить, что происходит при его недоступности, кто обновляет сертификаты webhook, как откатить ошибочное правило и где увидеть отклонённые запросы. «Fail-open» снижает защиту во время сбоя; «fail-closed» может заблокировать развёртывания. Выбор зависит от риска и должен быть осознанным.</p> <h2>Pod Security Admission: встроенная защита запуска Pod</h2> <p>Kubernetes имеет встроенный Pod Security Admission (PSA), который применяет уровни Pod Security Standards на уровне namespace. У PSA три режима: <code>warn</code> показывает пользователю предупреждение, <code>audit</code> добавляет аннотацию в audit event, <code>enforce</code> отклоняет Pod с нарушением. Стандарты имеют уровни <code>privileged</code>, <code>baseline</code> и <code>restricted</code>.</p> <p>Это хороший базовый контроль опасных параметров Pod, но не политика всего бизнеса. PSA не знает список разрешённых образов, не оценивает CVE и не определяет, имеет ли микросервис право обращаться к базе. Его задача уже и яснее: задать согласованный минимум безопасных параметров запуска.</p> <p>Начните с наблюдения в одном прикладном namespace. Версию стандарта лучше закрепить под используемую версию Kubernetes: так обновление кластера не меняет требования неожиданно.</p> <pre><code class="language-bash">kubectl label namespace payments \ pod-security.kubernetes.io/warn=restricted \ pod-security.kubernetes.io/audit=restricted \ pod-security.kubernetes.io/warn-version=v<ваша-версия-Kubernetes> \ pod-security.kubernetes.io/audit-version=v<ваша-версия-Kubernetes> </code></pre> <p>Плейсхолдер <code>v<ваша-версия-Kubernetes></code> нужно заменить конкретной поддерживаемой minor-версией, например <code>v1.37</code> только если именно эта политика выбрана для кластера. После исправления предупреждений добавляют <code>enforce=restricted</code> на пилотный namespace. Перед этим проверьте DaemonSet, Jobs и контроллеры: Pods часто создаются не напрямую, а из шаблонов workload, и принуждение в итоге применяется к созданному Pod.</p> <h2>Runtime: обнаружение не равно предотвращению</h2> <p>Даже проверенный и ограниченный образ может быть скомпрометирован уязвимостью приложения. Runtime-слой ищет сигналы уже после запуска: неожиданный shell, запись в чувствительный путь, запуск бинарника из временного каталога, обращение к необычному адресу, попытку получить привилегии. Конкретный охват зависит от ОС, runtime, датчика и его настроек.</p> <p>Сильный runtime-сигнал должен вести к готовой процедуре, а не только к алерту. Минимальный сценарий:</p> <ol> <li>Зафиксировать namespace, Pod, контейнер, образ по digest, узел и время события.</li> <li>Оценить критичность и сохранить логи/метаданные до перезапуска.</li> <li>Ограничить воздействие: изолировать workload сетевой политикой, остановить rollout или удалить скомпрометированный Pod — действие выбирается по runbook и критичности сервиса.</li> <li>Проверить соседние workloads с тем же digest и доступы service account.</li> <li>Устранить первопричину: обновить образ, правило или секрет, затем задокументировать выводы.</li> </ol> <p>Автоматическое удаление Pod по любому сигналу может усугубить инцидент или дать атакующему простой сервиса. Автоматизировать можно только заранее проверенные, обратимые и ограниченные действия; всё остальное требует контекста дежурного.</p> <h2>Как понять, что программа работает</h2> <p>Измеряйте не число установленных инструментов, а закрытие пути. Доля production-workloads с digest, доля образов с актуальным результатом сканирования, число временных исключений старше срока, доля namespaces с PSA в <code>enforce</code>, время от runtime-сигнала до triage и полнота связи «Pod → digest → сборка» — полезные показатели. Они показывают, где защита реальна, а где остаётся декларацией.</p> <p>Итог: supply chain предотвращает попадание неподходящего артефакта, PSA ограничивает опасный способ его запуска, а runtime даёт шанс обнаружить ситуацию, которую ранние фильтры не остановили. Все три слоя нужны, но у каждого своя честная граница.</p> <h2>Официальные источники</h2> <ul> <li>Kubernetes: Images — <a href="https://kubernetes.io/docs/concepts/containers/images/">https://kubernetes.io/docs/concepts/containers/images/</a></li> <li>Kubernetes: Admission controllers — <a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/">https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/</a></li> <li>Kubernetes: Pod Security Admission — <a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">https://kubernetes.io/docs/concepts/security/pod-security-admission/</a></li> <li>Kubernetes: Pod Security Standards — <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">https://kubernetes.io/docs/concepts/security/pod-security-standards/</a></li> </ul>