Поговорим о политиках безопасности OPA в кубере. Обсудим на примерах зачем они нужны, в каких случаях они действительно помогут обезопасить, когда политики могут положить всю систему и как ими пользоваться в кубере. Плюсом захватим немного кода на go для работы с ними.
Введение
В наше время сплошь и рядом мы видим информацию о том, что личные данные миллионов пользователей сливается. Видим нарушения работы разных крупных систем, например, 28 июля были проблемы у Аэрофлота из-за чего отменилось много рейсов, люди спали буквально в аэропорте и упала капитализация компании.
Политики безопасности нужны для соблюдения определенных правил компании. Сегодня мы обсудим Open Policy Agent. Инструмент довольно мощный и универсальный, если хотите детального погружения рекомендую погрузиться в официальную документацию.
Базовые случаи использования политик:
-
Может проверять манифесты
Deployment,StatefulSet,DaemonSet— например, обязательныеlabelsи аннотации. -
Для
Pod'ов— запрет запуска не из доверенных репозиториев или запрет тегаlatest -
Проверка событий вне
Pod'овСоздание/удалениеNamespacesтолько определёнными пользователями. И многое другое.
Как OPA работает в k8s
Обычно используют OPA Gatekeeper для интеграции OPA в k8s:
-
Он подключается как ValidatingAdmissionWebhook(руже MutatingAdmissionWebhook).
-
При каждом запросе на создание/изменение ресурса OPA получает объект в JSON.
-
Прогоняет его через политики, написанные на языке Rego.
-
Если что-то не проходит — возвращает ошибку, и объект не создаётся.
Немного о синтаксисе Rego
Rego декларативный язык, который очень похож на современные.
Базовый синтаксис будет показан ниже.
package k8sallowedrepos # Вспомогательное правило: true, если image начинается хотя бы с одного из разрешённых префиксов allowed_image(image) { allowed_repo := input.parameters.repos[_] startswith(image, allowed_repo) } # Правило нарушения для обычных контейнеров violation[{"msg": msg}] { # Перебираем все контейнеры в Pod container := input.review.object.spec.containers[_] # Если ни один из разрешённых префиксов не подходит под образ контейнера not allowed_image(container.image) # Формируем сообщение о нарушении msg := sprintf("container image '%v' is not from an allowed repository", [container.image]) }
Самая важная часть — input, немного разберем его детали
-
input.request.kind— указывает тип объекта k8s (например, Pod, Service и т. д.). -
input.request.operation— указывает тип операции: CREATE, UPDATE, DELETE или CONNECT. -
input.request.userInfo— содержит сведения об идентичности вызывающего пользователя. -
input.request.object— содержит весь объект Kubernetes. -
input.request.oldObject— содержит предыдущую версию объекта Kubernetes при операциях UPDATE и DELETE.
Установка OPA gatekeeper
Для работы у нас уже должен быть установлен кубер(подойдет minikube или k3s) и helm. После устанавливаем в helm gatekeeper.
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm install gatekeeper/gatekeeper --name-template=gatekeeper --namespace gatekeeper-system --create-namespace
Для проверки корректной установки вводим команду kubectl get pods -n gatekeeper-system в ответе должны быть созданы поды:
Создаем политики
Для создания политики нам нужно создать constraint template и constraint. Оба объекта можно сравнить с функцией(template) и передаваемых в нее данных(constraint).
Ниже будет базовый пример Template, который будет ограничивать создание подов из неразрешенных репозиториев. Базовый файл для создания объектов кубера + rego код для валидации.
apiVersion: templates.gatekeeper.sh/v1beta1 kind: ConstraintTemplate metadata: name: k8sallowedrepos # Имя шаблона политики spec: crd: spec: names: kind: K8sAllowedRepos # Тип Constraint, который будет использовать этот шаблон validation: # Определяем схему параметров, которые можно передать в Constraint openAPIV3Schema: properties: repos: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh # Указываем, что это webhook для admission контроля rego: | package k8sallowedrepos allowed_image(image) { allowed_repo := input.parameters.repos[_] startswith(image, allowed_repo) } violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not allowed_image(container.image) msg := sprintf("container image '%v' is not from an allowed repository", [container.image]) }
Далее создадим 2 constraint т.е. 2 набора правил для политики, которые будут жить автономно.
Первый constraint нужен для того, чтобы сервисы бекенда(смотрит на лейбл labelSelector) могли запускать поды только из разрешенных репозиториев(repos), иначе — не запустит.
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sAllowedRepos metadata: name: allow-backend-repos spec: match: kinds: - apiGroups: [""] kinds: ["Pod"] # Фильтр по лейблу "service=backend" labelSelector: matchLabels: service: backend parameters: repos: - "myregistry.com/backend/" - "python"
Второй constraint работает так же, как и первый, только относительно сервисов фронтенда.
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sAllowedRepos metadata: name: allow-frontend-repos spec: match: kinds: - apiGroups: [""] kinds: ["Pod"] # Применяется к Pod'ам # Фильтр по лейблу "service=frontend" labelSelector: matchLabels: service: frontend parameters: repos: - "myregistry.com/frontend/" - "nginx"
Теперь применим политику в k8s. Если все предыдущие шаги прошли корректно — делаем apply всех yaml файлов. Важно чтобы Template был создан первым так как Constraint цепляются именно к нему.
Применяем Template kubectl apply -f ct.yaml при успешном применении получим constrainttemplate.templates.gatekeeper.sh/k8sallowedrepos created.
Теперь приступим к применению constraint kubectl apply -f cons-front.yaml если все хорошо получим k8sallowedrepos.constraints.gatekeeper.sh/allow-frontend-repos created. Тоже самое нужно сделать с yaml бекенда.
Нарушаем политики
Приступим к самому интересному — нарушим политики! Сначала попробуем поднять наш под kubectl apply -f backend-pod-bad.yaml
apiVersion: v1 kind: Pod metadata: name: backend-bad labels: service: backend spec: containers: - name: app image: nginx:latest # запрещённый образ для backend
И OPA не дает нам этого сделать, ведь наш образ не из доверенного репозитория! Последняя часть ответа получена из нашего Template.
Error from server (Forbidden): error when creating «backend-pod-bad.yaml»: admission webhook «validation.gatekeeper.sh» denied the request: [allow-backend-repos] container image ‘nginx:latest’ is not from an allowed repository
Похожая ситуация будет, если мы попробуем запустить «плохой» под для сервисов фронтенда kubectl apply -f frontend-pod-bad.yaml
apiVersion: v1 kind: Pod metadata: name: frontend-bad labels: service: frontend spec: containers: - name: app image: alpine:latest # запрещённый образ
Получим практически идентичную ошибку
Error from server (Forbidden): error when creating «frontend-pod-bad.yaml»: admission webhook «validation.gatekeeper.sh» denied the request: [allow-frontend-repos] container image ‘alpine:latest’ is not from an allowed repository
Обратите внимание, что благодаря лейблам при попытке создать под фронта идет обращение к констрейнту фронта — allow-frontend-repos, а при беке — к констрейнту бека allow-backend-repos.
Но если мы захотим создать «хороший» под — kubectl apply -f backend-pod-good.yaml он создастся без нарушений pod/backend-good created .
Пример «хорошего» пода:
apiVersion: v1 kind: Pod metadata: name: backend-good labels: service: backend spec: containers: - name: app image: python:3.12-slim # разрешённый, т.к. начинается с "python" command: ["python", "-c", "print('Hello from Python Pod')"]
Интеграция с GO
Сделаем простой скрипт, который создаст Template и Constraint политики. Скрипт звезд с неба не хватает, но он работает. При работе с политиками есть достаточно нюансов, например если при создании Template тебе написали «created» — не обязательно, что он полностью создан и нужно подождать так как k8s работает асинхронно. Но подобные моменты были опущены для упрощения понимания.
Создаем Template и Constraint без регистрации и СМС
package main import ( "context" "fmt" "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" ) func main() { ctx := context.Background() // Подключаемся к Kubernetes из kubeconfig по умолчанию cfg, err := config.GetConfig() if err != nil { panic(err) } cli, err := client.New(cfg, client.Options{}) if err != nil { panic(err) } // Создаём ConstraintTemplate constraintTemplate := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "templates.gatekeeper.sh/v1beta1", "kind": "ConstraintTemplate", "metadata": map[string]interface{}{ "name": "k8sallowedrepos", }, "spec": map[string]interface{}{ "crd": map[string]interface{}{ "spec": map[string]interface{}{ "names": map[string]interface{}{ "kind": "K8sAllowedRepos", }, "validation": map[string]interface{}{ "openAPIV3Schema": map[string]interface{}{ "properties": map[string]interface{}{ "repos": map[string]interface{}{ "type": "array", "items": map[string]interface{}{"type": "string"}, }, }, }, }, }, }, "targets": []interface{}{ map[string]interface{}{ "target": "admission.k8s.gatekeeper.sh", "rego": ` package k8sallowedrepos allowed_image(image) { allowed_repo := input.parameters.repos[_] startswith(image, allowed_repo) } violation[{"msg": msg}] { container := input.review.object.spec.containers[_] not allowed_image(container.image) msg := sprintf("container image '%v' is not from an allowed repository", [container.image]) } violation[{"msg": msg}] { container := input.review.object.spec.initContainers[_] not allowed_image(container.image) msg := sprintf("initContainer image '%v' is not from an allowed repository", [container.image]) } `, }, }, }, }, } err = cli.Patch(ctx, constraintTemplate, client.Apply, client.ForceOwnership, client.FieldOwner("example")) if err != nil { fmt.Println("Create or update ConstraintTemplate failed, try Create") err = cli.Create(ctx, constraintTemplate) if err != nil { panic(err) } } fmt.Println("ConstraintTemplate created or updated") // Ждем пару секунд, чтобы CRD и шаблон успели зарегистрироваться в API // По-хорошему нужна проверка, что CRD и шаблон зарегистрированы в API time.Sleep(3 * time.Second) // Создаем Constraint constraint := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "constraints.gatekeeper.sh/v1beta1", "kind": "K8sAllowedRepos", "metadata": map[string]interface{}{ "name": "allow-backend-repos", }, "spec": map[string]interface{}{ "match": map[string]interface{}{ "kinds": []interface{}{ map[string]interface{}{ "apiGroups": []interface{}{""}, "kinds": []interface{}{"Pod"}, }, }, "labelSelector": map[string]interface{}{ "matchLabels": map[string]interface{}{ "service": "backend", }, }, }, "parameters": map[string]interface{}{ "repos": []interface{}{ "myregistry.com/backend/", "python", }, }, }, }, } err = cli.Create(ctx, constraint) if err != nil { fmt.Printf("Failed to create Constraint: %v\n", err) } else { fmt.Println("Constraint created") } }
Пример вывода с терминала с проверкой созданных политик
admin@MacBook-Pro-2 create_policy % go run main.go ConstraintTemplate created or updated Constraint created admin@MacBook-Pro-2 create_policy % kubectl get constrainttemplates NAME AGE k8sallowedrepos 8s admin@MacBook-Pro-2 create_policy % kubectl get constraint NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS allow-backend-repos deny
Так же интеграции с k8s и gatekeeper довольно мощный инструмент, с которым можно отслеживать нарушения политик, все операции по созданию политик и многое другое. Но здесь стоит быть очень аккуратным так как при работе много нюансов.
Вместо вывода(ломаем все одной политикой)
Политики безопасности очень мощный и полезный инструмент в хороших руках. Но в плохих можно сделать подобный rego код и любая попытка создать ресурс вернёт ошибку с сообщением "Creating or updating resources is forbidden by denyall policy". Будьте аккуратны!
package denyall violation[{"msg": msg}] { msg := "Creating or updating resources is forbidden by denyall policy" }
P.S. если создадите для тестов(только не на проде, прошу) подобную политику, чтобы вернуть все в рабочий вид — удалите ваш Template.
ссылка на оригинал статьи https://habr.com/ru/articles/935842/
Добавить комментарий