Политики безопасности k8s gatekeeper OPA. Интеграция с GO

от автора

Поговорим о политиках безопасности OPA в кубере. Обсудим на примерах зачем они нужны, в каких случаях они действительно помогут обезопасить, когда политики могут положить всю систему и как ими пользоваться в кубере. Плюсом захватим немного кода на go для работы с ними.

Введение

В наше время сплошь и рядом мы видим информацию о том, что личные данные миллионов пользователей сливается. Видим нарушения работы разных крупных систем, например, 28 июля были проблемы у Аэрофлота из-за чего отменилось много рейсов, люди спали буквально в аэропорте и упала капитализация компании.

Динамика акций на 28 июля

Динамика акций на 28 июля

Политики безопасности нужны для соблюдения определенных правил компании. Сегодня мы обсудим Open Policy Agent. Инструмент довольно мощный и универсальный, если хотите детального погружения рекомендую погрузиться в официальную документацию

Базовые случаи использования политик:

  • Может проверять манифесты Deployment, StatefulSet, DaemonSet — например, обязательные labels и аннотации. 

  • Для Pod'ов — запрет запуска не из доверенных репозиториев или запрет тега  latest

  • Проверка событий вне Pod'ов Создание/удаление Namespaces только определёнными пользователями. И многое другое. 

Как OPA работает в k8s

Обычно используют OPA Gatekeeper  для интеграции OPA в k8s:

  • Он подключается как ValidatingAdmissionWebhook(руже MutatingAdmissionWebhook).

  • При каждом запросе на создание/изменение ресурса OPA получает объект в JSON.

  • Прогоняет его через политики, написанные на языке Rego.

  • Если что-то не проходит — возвращает ошибку, и объект не создаётся.

Принцип работы OPA с k8s

Принцип работы OPA с k8s

Немного о синтаксисе 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.

Есть ТГ(общаемся, постим всякое ITшное) и немного Ютуба.


ссылка на оригинал статьи https://habr.com/ru/articles/935842/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *