Поговорим о политиках безопасности 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.
Cyber_Griffin
А потом дописанное RDP ставит под удар всю выстроенную систему безопасности)))