Поговорим о политиках безопасности 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шное) и немного Ютуба.

Комментарии (1)


  1. Cyber_Griffin
    12.08.2025 05:06

    А потом дописанное RDP ставит под удар всю выстроенную систему безопасности)))