OPA Gatekeeper в Kubernetes: политики безопасности как код вместо зоопарка admission webhooks
OPA Gatekeeper v2 вышел в 2019: декларативные политики на Rego заменили нам несколько ручных admission webhooks и Pod Security Policy - теперь политики ревьюятся как код.
OPA Gatekeeper v2 выходит в 2019: декларативные политики безопасности как код на языке Rego вместо Pod Security Policy.
До Gatekeeper у нас в кластерах жил небольшой зоопарк. Admission webhook для проверки image registry - написан на Go, живёт в отдельном namespace, документация в голове у одного инженера. Ещё один webhook - для проверки resource limits, потому что некоторые команды деплоили поды без limits вообще и потом удивлялись, почему нода уходит в OOM. Pod Security Policy пытались использовать, но в итоге для половины workload-ов выдали максимально расслабленную политику, потому что разобраться в правилах PSP и не сломать всё вокруг - отдельный квест.
Это жило, работало, но каждый новый кластер требовал ручной установки всего этого хозяйства. Версии могли расходиться. Изменение одного webhook-а - ручная сборка образа, ручной деплой, надежда что в staging проверили правильно.
Что такое Gatekeeper и при чём тут OPA
Open Policy Agent - это движок политик, написанный на Go, с собственным языком запросов Rego. Он существует уже несколько лет и активно используется за пределами Kubernetes - для авторизации в микросервисах, в Terraform Enterprise, в системах CI/CD. Kubernetes-специфичная часть - Gatekeeper - это обёртка, которая делает OPA полноценным admission controller-ом: принимает admission request от API-сервера, прогоняет через политики, возвращает Allow или Deny с понятным текстом ошибки.
Версия v2, которая вышла в этом году, принесла главное: ConstraintTemplate и Constraint как нативные CRD. Это меняет способ работы с политиками принципиально.
Раньше политики OPA хранились в ConfigMap и синхронизировались в runtime. Теперь каждый тип ограничения - это CRD. Хочешь политику «все поды обязаны иметь label team» - создаёшь ConstraintTemplate с логикой на Rego, потом Constraint, который применяет эту политику к конкретным namespace-ам. Оба ресурса - обычные YAML-файлы, которые живут в Git.
Как выглядит это на практике
Первую политику подняли самую простую: запрет образов не из нашего internal registry. Rego-правило занимает десяток строк:
package k8srequiredregistry
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not startswith(container.image, "registry.company.internal/")
msg := sprintf("image %v не из разрешённого registry", [container.image])
}
ConstraintTemplate оборачивает этот Rego в CRD, Constraint определяет где применять. Всё. Когда кто-то попытается задеплоить под с образом с Docker Hub - получит внятное сообщение об ошибке прямо в kubectl.
Вторая политика - обязательные resource requests и limits. Третья - запрет privileged-контейнеров в production namespace-ах. К концу недели у нас было шесть политик, и все они жили в Git рядом с остальной инфраструктурой.
Что произошло с admission webhooks
Кастомные webhooks мы не удалили разом - это было бы неосторожно. Сначала перевели логику в Gatekeeper-политики, запустили параллельно в режиме аудита (Gatekeeper умеет работать в audit mode: записывает нарушения в статус Constraint, но не блокирует), убедились что политики ловят то же самое что webhook. После этого webhook отключили.
Audit mode - полезная вещь сама по себе. После включения он пробежался по существующим ресурсам в кластере и нашёл несколько подов, которые нарушали новые политики и уже давно жили в кластере. Кто-то задеплоил руками через kubectl apply с локальной машины, где kubeconfig был настроен в обход CI/CD. Без Gatekeeper мы бы не знали.
Pod Security Policy vs Gatekeeper
PSP в Kubernetes работает, но у него есть неудобное свойство: включение PSP - отдельный feature gate, и применение политики к workload зависит от того, какой ServiceAccount использует pod и какие RoleBinding-и у него есть. Это не то чтобы сложно, но легко запутаться, и диагностика почему конкретный под не запускается - не самый приятный процесс.
В Gatekeeper логика другая: Constraint применяется к namespace-ам через labelSelector, и понятно из YAML-файла - какая политика работает где. Rego-правило читается - там видно, что именно проверяется. Если нарушение - сообщение пишет сам Rego и разработчик видит осмысленный текст, а не generic «pod security policy violation».
PSP мы не убираем: в кластерах где он был включён, он работает. Просто новую логику пишем через Gatekeeper.
Ревью политик как кода
Это, пожалуй, главное изменение в процессе. Раньше изменение webhook-а - это PR в отдельный репозиторий, сборка Docker-образа, pipeline, деплой. Два-три участника процесса, час-два времени даже для простого изменения.
Теперь изменение политики - это PR с изменением YAML-файла. В managed-инфраструктуре мы положили политики в тот же GitOps-репозиторий, где живут остальные кластерные ресурсы. Argo CD синхронизирует. Изменение политики проходит через тот же ревью-процесс, что изменение Deployment или ConfigMap.
Это не только удобство - это аудит. Когда через три месяца кто-то спросит «почему у нас нельзя деплоить из Docker Hub», ответ будет в git log с датой, автором и описанием PR.
Что пока шероховато
Rego - язык непривычный. Он декларативный в стиле Datalog, и люди с бэкграундом в Python или Go поначалу пишут Rego как будто это процедурный код, а потом удивляются почему правило не работает. Несколько часов ушло на то, чтобы понять семантику правил с несколькими violation-блоками.
Документация на Rego есть, playground на play.openpolicyagent.org удобный - там можно отладить правило с реальным admission request-ом до того, как применять в кластере. После первых нескольких политик становится легче.
Инструментария для тестирования политик пока немного. OPA умеет гонять unit-тесты на Rego, и мы начали их писать, но это требует дисциплины - пока тестовое покрытие не всех политик.
В целом: направление правильное, и для нас это закрывает больше вопросов, чем создаёт.