Перейти к содержимому

OPA в CI/CD для Kubernetes: блокируем privileged-контейнеры до деплоя

OPA в CI/CD для Kubernetes: блокируем privileged-контейнеры до деплоя
Время чтения: 10 мин.

На аудите Kubernetes-кластера одной финтех-компании я нашёл 14 Deployment’ов с privileged: true в production. Ноль policy-проверок на этапе CI, admission controller отсутствует как класс. За два дня показал команде: из любого из этих контейнеров можно за 30 секунд получить root на ноде и вытащить секреты соседних подов. Проблема не в разработчике, который поставил флаг. Проблема в пайплайне, который пропустил это молча. Ниже — рабочий рецепт: пишем Rego-политику, проверяем манифесты через Conftest и встраиваем блокировку privileged-контейнеров в CI так, чтобы небезопасный манифест не дошёл до кластера.

Чем опасен privileged-контейнер в Kubernetes

Флаг privileged: true в securityContext контейнера снимает почти все ограничения изоляции. Контейнер получает полный набор Linux capabilities, прямой доступ к устройствам хоста через /dev и возможность монтировать файловые системы ноды. Граница между контейнером и хостовой ОС — стирается.

Если вы ещё не сталкивались с MITRE ATT&CK — это открытая база тактик и техник реальных атак. Каждая техника имеет свой идентификатор (T-код). Privileged-контейнер открывает прямой путь к технике Escape to Host (T1611, тактика Privilege Escalation — повышение привилегий в системе). На практике цепочка атаки выглядит так:

  1. Атакующий компрометирует веб-приложение через уязвимость в коде или зависимостях — Deploy Container (T1610, Execution).
  2. Обнаруживает privileged: true — монтирует файловую систему хоста через mount /dev/sda1 /mnt или входит в namespace хоста командой nsenter --target 1 --mount --uts --ipc --net --pid.
  3. Читает kubeconfig, токены сервисных аккаунтов соседних подов — Container API (T1552.007, Credential Access).
  4. Проводит разведку кластера — Container and Resource Discovery (T1613, Discovery).
  5. Lateral movement через Kubernetes API с украденным токеном.

По данным CrowdStrike Global Threat Report 2025, среднее время lateral movement после первичного доступа — 62 минуты, а рекорд — 51 секунда. С privileged-контейнером шаги 2–4 укладываются в секунды. Не минуты — секунды.

Бизнес-последствия: утечка данных из баз соседних сервисов, компрометация секретов (API-ключи, TLS-сертификаты, credentials баз данных), в худшем случае — полный контроль над кластером. Это прямое попадание в OWASP Top 10 A05:2021 — Security Misconfiguration (когда конфигурация по умолчанию небезопасна, а настройки неполные).

Ловить такое на ревью YAML-манифестов — ненадёжно. В Deployment’е из 50 строк privileged: true прячется внутри securityContext одного из контейнеров. Глаз замыливается. Нужна автоматизированная проверка манифестов Kubernetes перед деплоем.

OPA и Conftest — policy as code для безопасности Kubernetes

Open Policy Agent (OPA, произносится «oh-pa») — универсальный движок проверки политик с открытым кодом, graduated-проект CNCF. Если аббревиатура CNCF вам пока не встречалась: Cloud Native Computing Foundation — организация, которая развивает Kubernetes, Prometheus и другие стандарты облачной инфраструктуры. Graduated-статус означает, что проект прошёл аудит зрелости и используется в production крупными компаниями. OPA принимает структурированные данные (JSON, YAML) и проверяет их по правилам на языке Rego (произносится «ray-go»).

Аналогия для тех, кто приходит из разработки: OPA — это линтер, только проверяет не код приложения, а конфигурацию инфраструктуры. Как ESLint ловит console.log в production-коде, так OPA ловит privileged: true в Kubernetes-манифесте. Policy as code для Kubernetes — подход, при котором правила безопасности описаны декларативно, хранятся в Git, проходят ревью и проверяются автоматически при каждом изменении. Не устные договорённости «мы так не делаем», а код, который не даст сделать.

Conftest — CLI-инструмент поверх OPA, заточенный под CI/CD-пайплайны. Принимает на вход YAML, JSON, HCL (Terraform), Dockerfile и проверяет по Rego-политикам из указанной директории. Установка: brew install conftest на macOS, либо бинарник со страницы Conftest Releases на GitHub. Главное свойство для CI: возвращает exit code 1 при нарушении — любая CI-система (GitLab CI, GitHub Actions, Jenkins) интерпретирует это как failed job.

Здесь стоит разобраться в разнице с admission controller’ом. OPA Gatekeeper — это контроллер допуска, который перехватывает запросы к Kubernetes API и работает на этапе admission, когда kubectl apply уже отправляет манифест в кластер. Conftest в CI работает раньше — на этапе pull request, до мёрджа. Обратная связь приходит разработчику прямо в merge request, а не в логах деплоя. В зрелой конфигурации используют оба уровня, но начинать стоит с CI — не требует прав на кластер и даёт результат за один день.

Пишем Rego-политику: блокировка privileged-контейнеров

Rego — декларативный язык запросов для работы с иерархическими JSON-структурами. Если писали SQL-запросы, логика покажется знакомой: описываете условие — движок находит все совпадения.

Каждая Rego-политика для Conftest строится из трёх элементов. package main — Conftest по умолчанию ищет пакет main (можно переопределить флагом --namespace). deny[msg] — набор правил-запретов; если все условия в теле правила истинны, правило срабатывает и добавляет сообщение msg в список нарушений. input — ваш YAML-документ, автоматически преобразованный в JSON при вызове Conftest.

Создайте файл policy/privileged.rego:

package main

deny[msg] {
  input.kind == "Deployment"
  container := input.spec.template.spec.containers[_]
  container.securityContext.privileged == true
  msg := sprintf(
    "Container '%s': privileged mode запрещён.",
    [container.name])
}

Разбор построчно. input.kind == "Deployment" — политика применяется только к ресурсам типа Deployment. Для покрытия Pod, StatefulSet, DaemonSet добавьте аналогичные правила или обобщите через helper-функцию. container := input.spec.template.spec.containers[_] — итерация по всем контейнерам в спецификации пода; [_] — wildcard-индекс, OPA проверит каждый элемент массива. container.securityContext.privileged == true — условие срабатывания. sprintf формирует человекочитаемое сообщение с именем контейнера — именно его разработчик увидит в логах CI.

Структура проекта простая: директория k8s/ с манифестами и policy/ с Rego-файлами. Один файл — одна политика: не обязательно, но так удобнее поддерживать и тестировать. Для расширения добавьте policy/tags.rego (запрет :latest-тегов), policy/resources.rego (требование CPU/memory limits), policy/security_context.rego (требование runAsNonRoot: true).

Проверка манифестов Kubernetes перед деплоем через Conftest

Прежде чем встраивать в CI — проверьте локально. Нужен установленный conftest CLI.

Допустим, в k8s/deployment.yaml лежит Deployment с контейнером nginx, у которого securityContext: {privileged: true}. Запустите conftest test k8s/deployment.yaml --policy policy/. Conftest распарсит YAML в JSON, загрузит все .rego-файлы из policy/ и прогонит манифест по каждому правилу deny.

Ожидаемый вывод при нарушении: FAIL - k8s/deployment.yaml - main - Container 'nginx': privileged mode запрещён. и итого 1 test, 0 passed, 0 warnings, 1 failure. Exit code — 1.

Как убедиться, что исправление сработало: уберите privileged: true из манифеста (или поставьте privileged: false) и запустите повторно. Вывод: 1 test, 1 passed, 0 warnings, 0 failures. Exit code — 0. Готово.

Для проверки всех манифестов в директории: conftest test k8s/ --policy policy/. Флаг -o table форматирует вывод таблицей, -o json — выдаёт JSON для дальнейшей обработки. Этот статический анализ Kubernetes-манифестов занимает меньше секунды даже на сотне файлов.

OPA Conftest в CI-пайплайне: GitLab CI и GitHub Actions

Цель — при каждом merge request автоматически проверять манифесты. Нарушена хотя бы одна политика — пайплайн падает, MR не мёрджится. Это ядро подхода shift-left security для Kubernetes: ловим ошибку конфигурации до того, как она попадёт в кластер.

GitLab CI

Добавьте job в .gitlab-ci.yml:

policy-check:
  stage: test
  image: openpolicyagent/conftest:latest
  script:
    - conftest test k8s/ --policy policy/ -o table
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

GitLab запускает контейнер с предустановленным Conftest, проверяет все YAML-файлы в k8s/ по политикам из policy/. Блок rules ограничивает запуск только merge request’ами — на каждый push в main проверка не нужна, хватит проверки до мёрджа. При нарушении exit code 1 — job помечается как failed. Если в настройках проекта включён «pipeline must succeed», MR невозможно смёрджить до исправления.

Проверить просто: создайте MR с Deployment’ом, содержащим privileged: true. В pipeline увидите failed job policy-check с сообщением из Rego-политики. Исправьте манифест — pipeline станет зелёным.

GitHub Actions

Для GitHub создайте .github/workflows/policy-check.yml с событием pull_request. Используйте action open-policy-agent/setup-opa для установки OPA и conftest, затем шаг run: conftest test k8s/ --policy policy/. При нарушении PR получает красный check. В документации OPA есть готовый пример workflow с этим action, включая проверку через opa eval --fail-defined для более сложных сценариев.

Обе конфигурации используют одни и те же Rego-политики — меняется только CI-обёртка. Политики живут в Git рядом с манифестами и проходят ревью вместе с ними. Это DevsecOps CI/CD на практике: безопасность встроена в процесс разработки, а не прикручена сбоку.

Тестирование Rego-политик: opa test как юнит-тесты

Политика без тестов — как код без unit-тестов: работает до первой правки. Опечатка в пути (spc вместо spec) — и политика молча пропускает всё подряд. OPA поддерживает встроенное тестирование: правила с префиксом test_ проверяют ожидаемое поведение на фиксированных входных данных.

Создайте файл policy/privileged_test.rego:

package main

mock := {"kind": "Deployment", "spec": {"template":
  {"spec": {"containers": [{"name": "app",
    "securityContext": {"privileged": true}}]}}}}

test_deny_privileged {
  count(deny) > 0 with input as mock
}

Запустите opa test policy/ -v (нужен установленный opa CLI — brew install opa или бинарник с GitHub). Флаг -v показывает результат каждого теста. Ожидаемый вывод: policy/privileged_test.rego: PASS (1/1).

Добавьте обратный тест — манифест без privileged: true должен проходить проверку. Создайте второй mock-объект без securityContext.privileged и проверьте count(deny) == 0 with input as mock_safe. Два теста покрывают оба сценария: правило срабатывает когда должно и молчит когда не должно.

Тесты запускаются в CI отдельным шагом перед conftest test — это гарантирует, что сами политики корректны. Если тест упал — манифесты даже не начнут проверяться. Экономит время на отладку.

Shift-left security или admission controller Kubernetes

Проверка в CI (shift-left) и OPA Gatekeeper (admission controller Kubernetes) решают одну задачу на разных уровнях. CI ловит ошибку раньше — разработчик получает обратную связь в merge request до мёрджа. Gatekeeper ловит то, что прошло мимо CI: ручной kubectl apply, Helm-релиз из терминала, манифест от оператора, который не проходил через Git.

Статический анализ Kubernetes-манифестов в CI не покрывает динамические сценарии: Helm-шаблоны с переменными вычисляются при рендеринге, Kustomize-overlays меняют securityContext через патчи, операторы генерируют манифесты на лету. Для этого нужен admission controller, который видит финальный манифест.

Рекомендация: начните с CI через Conftest — даёт результат за день, не требует прав на кластер. Когда политики стабилизируются — разверните OPA Gatekeeper с Constraint Templates (CRD-ресурсы, описывающие правила через Rego) как второй рубеж. Rego-код переносится между Conftest и Gatekeeper с минимальными изменениями. Не «или/или», а два уровня одной защиты.

Альтернатива OPA — Kyverno: политики описываются на YAML без изучения Rego, что проще для старта. Но Kyverno работает только с Kubernetes-ресурсами, а OPA — универсальный движок, применимый к Terraform, Dockerfile, CI-конфигам и API-авторизации. Если задача ограничена Kubernetes — Kyverno проще. Если нужна единая policy-as-code платформа для всего стека DevsecOps — OPA остаётся стандартом.

Большинство команд, которые приходят к OPA, начинают с Gatekeeper на кластере. Это ошибка приоритизации. Admission controller — последний рубеж. Он ловит то, что прошло мимо всего остального. Но если «всего остального» нет, вы узнаёте о проблеме в момент деплоя, когда разработчик уже закоммитил, замёрджил и ждёт результата. Обратная связь на этом этапе воспринимается как блокер, а не как помощь.

Conftest в CI — это разница между «твой MR не прошёл проверку, вот что поправить» и «деплой упал, разбирайся с admission webhook’ом». Первый сценарий работает как линтер — люди принимают линтеры. Второй — как запрет, и ему сопротивляются.

За два года я внедрял OPA-проверки в восьми командах. В шести начинали с Gatekeeper, четыре из них через месяц выключали enforcement mode — «мешает деплоить». Две команды, которые начали с CI, обе сохранили enforcement и позже добавили Gatekeeper без сопротивления. Порядок внедрения определяет, приживутся ли политики.

Если переходите в ИБ из разработки или ops — на codeby.school курс IB Basics закрывает фундамент (модель угроз, классификация данных, compliance), который OPA-политики не заменят.

Эту тему и смежные навыки разбирают на практике в курсе «Профессия DevSecOps-инженер» Codeby Academy.