Статья Kubernetes Admission Controllers: Использование Kyverno для enforcement security policies

1774982486901.webp
Kyverno - это такой «полицейский, который говорит на твоём языке». Не Rego, не JSONnet, а простой YAML. Тот самый YAML, который ты пишешь каждый день. И да, он не просто проверяет - он может мутировать ресурсы на лету, генерировать новые, сканировать уже существующий мусор и давать тебе понятные отчёты. Мы разберём его до винтиков: как установить так, чтобы в 3 часа ночи вебхук не рухнул, как написать политики, которые реально спасут тебя от дурацких ошибок, и как потом доказать аудиту, что вы «security first».

Это не статья для новичков, которые только kubectl get pods выучили. Это гайд для тех, кто уже набил шишки на --privileged, кто ловил OOMKilled потому что кто-то не поставил limits, и кто устал объяснять, почему :latest в продакшене - это медленно текущая язва.

Держись, будет жёстко, честно, с примерами, которые можно копипастить прямо сейчас, и с подводными камнями, о которых молчат документации. Погнали.


1. Роль Admission Controllers в K8s: почему твой​

Ты привык, что kubectl apply -f manifest.yaml - это священнодействие. Но на самом деле, когда ты жмёшь Enter, твой запрос проходит через цепочку обработчиков, которые могут как молча пропустить его, так и разорвать в клочья, даже если синтаксис идеален.

1.1. Жизненный цикл API-запроса: от клиента до etcd​

Давай быстро пробежимся по тому, что происходит, когда ты запускаешь команду. Это не просто «отправил запрос - получил ответ». Это целый конвейер, похожий на досмотр в аэропорту, только вместо бабушек с чемоданами - твои поды и сервисы.
  1. Аутентификация (Authentication)
    Ты стучишься в API-сервер. Кто ты? Токен, сертификат, клиентский сертификат? API-сервер проверяет твои учётные данные. Если не прошёл - 401 Unauthorized, иди лесом. Никакие политики тут не помогут, если ты не авторизован.
  2. Авторизация (Authorization)
    Ладно, ты прошёл. Теперь API-сервер смотрит: а можно ли тебе вообще делать то, что ты просишь? Тут вступают в игру RBAC (Role-Based Access Control), ABAC и прочие. Если твой ServiceAccount не имеет прав на создание Pod'ов в namespace production - получишь 403 Forbidden. Это уровень авторизации, он работает на основе ролей и правил.
  3. Admission Control (Контроль доступа)
    Вот тут начинается самое интересное. Ты уже аутентифицирован и авторизован. Ты имеешь право создать под. Но хорошо ли то, что ты создаёшь? Именно здесь живут Admission Controllers. Это последняя линия обороны перед тем, как объект попадёт в etcd.
Admission контроллеры делятся на две группы: изменяющие (Mutating) и проверяющие (Validating). Они могут быть встроенными в kube-apiserver (например, NamespaceLifecycle, LimitRanger) или внешними - через динамические вебхуки (MutatingAdmissionWebhook и ValidatingAdmissionWebhook).

Представь: ты создаёшь под. Запрос проходит мутационные вебхуки, которые могут подправить твой манифест (добавить метки, изменить securityContext, подставить sidecar). Потом проходят валидирующие вебхуки, которые проверяют итоговый объект (уже после мутаций!). Если хоть один из них скажет «нет» - запрос отклоняется, и ты видишь знакомое сообщение об ошибке.

И только после этого объект сохраняется в etcd. Это важно: если ты используешь и Mutating, и Validating вебхуки, мутации применяются перед валидацией. Поэтому политики валидации всегда видят уже изменённый объект.

Почему это важно для нас с Kyverno? Kyverno работает именно как динамический вебхук. Он регистрирует два вебхука: один mutating, другой validating. И он имеет возможность ещё и background scan - проверять уже существующие объекты, которые не были проверены при создании. Но об этом позже.

1.2. Mutating Admission Webhook - когда «сырьё»​

Mutating вебхук может изменить объект ДО того, как он будет зафиксирован. Он получает запрос в формате AdmissionReview, может добавить/изменить/удалить поля и вернуть изменённую версию. Это мощно и опасно одновременно. Потому что разработчик пишет один манифест, а в кластер попадает совсем другой.

Kyverno использует mutating вебхук для своих политик типа Mutate. Например, ты можешь автоматически добавить securityContext с runAsNonRoot: true в каждый под, который не имеет этого поля. Или вписать imagePullPolicy: Always для образов с тегом latest. Или принудительно добавить метку managed-by: kyverno. Всё это происходит незаметно для пользователя.

Важный момент: порядок выполнения мутационных вебхуков имеет значение. Если у тебя несколько мутационных вебхуков, они выполняются в порядке, определённом в webhooks[].matchPolicy и webhooks[].reinvocationPolicy. Kyverno по умолчанию использует reinvocationPolicy: "IfNeeded", что позволяет ему вызываться повторно, если другие вебхуки изменили объект после него. Это гарантирует, что политики Kyverno будут применяться к финальной версии объекта.

1.3. Validating Admission Webhook - последний страж​

После того как все мутации применены, наступает время валидации. Validating вебхук может сказать «да» или «нет». Если он отвечает allowed: false, запрос отклоняется, и пользователь получает ошибку. Kyverno использует validating вебхук для политик типа Validate. Здесь ты можешь запретить privileged: true, потребовать наличие resources.limits, проверить, что образ не содержит :latest, и так далее.

Но есть нюанс: если вебхук недоступен (например, Kyverno под упал), что делать? Это определяется failurePolicy. Fail - если вебхук не отвечает, запрос блокируется (безопасный подход, но может парализовать кластер). Ignore - пропускает запрос, но тогда политики не применяются. Выбор зависит от твоей толерантности к риску. В production я всегда начинаю с Ignore и постепенно, убедившись в надёжности, перехожу на Fail для критических политик.


2. Установка и настройка Kyverno в production: не просто​

Ты наверняка видел десяток туториалов, где установка Kyverno - это helm install kyverno kyverno/kyverno --namespace kyverno --create-namespace. И всё, готово. А потом в 3 часа ночи вебхук начинает таймаутиться, поды съедают всю память, и ты не можешь даже kubectl delete pod сделать, потому что валидация блокирует.

Давай настроим Kyverno так, чтобы он работал как часы, даже когда кто-то решит запустить kubectl apply -f . с тысячей объектов.

2.1. Helm chart: values для production (replicas, resources, webhookTimeoutSeconds)​

Kyverno официально распространяется через Helm chart. Но дефолтные значения - это для минимального теста. Для production нужно пересмотреть всё.

Начнём с того, что Kyverno состоит из нескольких компонентов:
  • kyverno - основной контроллер, который обрабатывает вебхуки и background сканирование.
  • kyverno-cleanup-controller - опциональный контроллер для политик очистки (Cleanup policies).
  • kyverno-reports-controller - контроллер, генерирующий PolicyReport и ClusterPolicyReport.
  • webhook - сертификаты, управляемые оператором.
Production-ready values.yaml:

YAML:
# Количество реплик - минимум 2 для HA. Но не забывай, что вебхук
# приходит на любой под, и если один под медленный, запросы могут таймаутиться.
replicaCount: 2

image:
  tag: v1.11.0  # Бери последний стабильный, но не latest

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 1
    memory: 1Gi

# Настройка вебхуков
admissionController:
  # Таймаут вебхука - по умолчанию 10 секунд. Если твои политики сложные,
  # могут быть задержки. Ставь 15-20, но помни: чем больше таймаут, тем дольше
  # пользователь ждёт ответа. kube-apiserver имеет глобальный таймаут 30 сек.
  webhookTimeoutSeconds: 15
  # Политика отказа: можно "Fail" или "Ignore".
  # Я рекомендую начинать с "Ignore", после проверки стабильности переключить на "Fail"
  failurePolicy: Ignore

# Background сканирование
backgroundScan:
  enabled: true
  # Интервал сканирования: по умолчанию 24h. Для активного кластера можно уменьшить,
  # но не чаще чем раз в час, чтобы не нагружать API.
  interval: 24h

# Конфигурация политик
policyController:
  # Количество воркеров для обработки политик
  workers: 4
  # Максимальное количество объектов, обрабатываемых одновременно
  maxQueuedEvents: 1000

# Сертификаты - важный момент. Kyverno использует собственный оператор для управления
# сертификатами вебхука. Он создаёт secret с сертификатом и обновляет его автоматически.
# В production убедись, что оператор имеет права на обновление вебхуков.
# Если ты используесть cert-manager, можешь задать custom certificate.
certManager:
  enabled: false  # Если используешь cert-manager, то true и настрой ниже
# customCert: ...

Почему я ставлю webhookTimeoutSeconds: 15? Потому что если у тебя есть политики, которые делают сложные сопоставления (например, проверка наличия образа в частном реестре через OCI), они могут выполняться дольше стандартных 10 секунд. 15 секунд - золотая середина. Но не ставь 30, потому что у API-сервера есть общий таймаут на запрос, и если он истечёт раньше, чем вебхук ответит, ты получишь ошибку, даже если политика была satisfied.

failurePolicy: Ignore - для первого развёртывания я всегда выбираю Ignore. Почему? Представь, что ты только установил Kyverno, и по какой-то причине его поды не запустились (например, нет места на узлах). Если failurePolicy = Fail, то любой создаваемый объект будет блокироваться, потому что вебхук недоступен. Кластер фактически парализован. Поэтому сначала ставим Ignore, наблюдаем за стабильностью, потом переключаем на Fail для критических политик, а для некритических оставляем Ignore.

2.2. Background scanning: проверка существующих ресурсов​

Одна из киллер-фич Kyverno - background scan. Обычные вебхуки работают только при создании/изменении объектов. А что с тем мусором, который уже живёт в кластере? Там могут быть поды с privileged: true, созданные до внедрения политик.

Kyverno запускает фоновые сканеры, которые периодически проверяют все существующие ресурсы на соответствие политикам. Результаты складываются в CRD PolicyReport (для неймспейсных ресурсов) и ClusterPolicyReport (для кластерных). Эти отчёты потом можно просмотреть через kubectl, экспортировать в метрики или отобразить в дашборде.

Важно: background scan не изменяет объекты и не удаляет их. Он только отчитывается о нарушениях. Это как аудит, который ты можешь показать начальнику: «Смотри, 50% подов нарушают политику запрета privileged». Дальше ты сам решаешь, автоматически ли исправлять их или ждать, пока разработчики пересоздадут.

Настройка background scan через values:

YAML:
backgroundScan:
  enabled: true
  interval: 12h  # каждые 12 часов
  # Можно ограничить количество одновременно сканируемых ресурсов
  maxWorkers: 10

Если у тебя кластер с тысячами объектов, не ставь интервал слишком маленьким - сканер будет постоянно грузить API-сервер. 24 часа - нормально для большинства случаев. Если ты часто меняешь политики и хочешь видеть изменения быстрее, можно поставить 6 часов, но мониторь нагрузку.

2.3. Failure policy: Fail vs Ignore - поведение при недоступности Kyverno​

Это, пожалуй, самый важный параметр, который может спасти или уничтожить твой кластер. Каждый вебхук (и mutating, и validating) имеет поле failurePolicy. Оно определяет, что делать, если вебхук не отвечает (таймаут, 500 ошибка, нет эндпоинта).
  • Fail: если вебхук недоступен, запрос отклоняется. Это безопасно с точки зрения безопасности, потому что ты не допустишь создание невалидных объектов в обход политик. Но если Kyverno лежит, то вообще ничего нельзя будет создать/обновить. Это как «сейфовый режим».
  • Ignore: если вебхук недоступен, запрос пропускается без проверки. Это позволяет кластеру продолжать работать, но политики перестают действовать.
В production я использую гибридный подход: для разных вебхуков разные политики. Но Kyverno использует единый вебхук для всех политик, поэтому failurePolicy настраивается глобально. Однако ты можешь переопределить его в конкретных политиках через поле failurePolicy (доступно в Kyverno 1.9+). Это позволяет сделать так: для политик, блокирующих privileged, установить failurePolicy: Fail, а для менее критичных - Ignore.

Как настроить в values:

YAML:
admissionController:
  failurePolicy: Ignore  # глобальная, но может быть переопределена в политике

Пример переопределения в политике:

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
spec:
  failurePolicy: Fail   # даже если Kyverno не в ресурсе, блокируем запрос
  validationFailureAction: Enforce  # Enforce или Audit
  rules:
    - name: privileged-container
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: "Privileged containers are not allowed."
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: false

Но будь осторожен: если ты используешь failurePolicy: Fail в политике, а сам Kyverno по какой-то причине недоступен, то запросы, подпадающие под эту политику, будут блокироваться, даже если ты глобально поставил Ignore. Это может привести к частичной блокировке кластера. Поэтому внедряй постепенно: сначала Audit режим, потом Enforce, и только потом Fail для критических политик.


3. Написание Validate-политик: искусство говорить «нет»​

Теперь самое вкусное - политики. Kyverno даёт два основных режима проверки: validate и validate.pattern. Но есть ещё validate.deny для сложных условий и validate.anyPattern для нескольких вариантов. Мы начнём с самых насущных проблем, которые встречаются в каждом кластере.

3.1. Запрет privileged: true и hostPID/hostNetwork​

Привилегированные контейнеры - это дыра в безопасности. Они имеют доступ ко всем устройствам хоста, могут эскалировать привилегии и делать что угодно. Обычно это нужно только в очень специфических случаях (например, DaemonSet для мониторинга), но разработчики любят включать privileged: true для удобства.

Запретим раз и навсегда.

Политика: disallow-privileged.yaml

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
  annotations:
    policies.kyverno.io/title: Disallow Privileged Containers
    policies.kyverno.io/category: Security
    policies.kyverno.io/severity: high
    policies.kyverno.io/subject: Pod
    policies.kyverno.io/description: >-
      Privileged containers can break out of container isolation and compromise
      the host. This policy ensures that no container is allowed to run with
      privileged: true.
spec:
  validationFailureAction: Enforce   # Enforce блокирует, Audit только пишет отчёт
  background: true                    # Сканировать существующие объекты
  rules:
    - name: privileged-containers
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: "Privileged containers are not allowed. Please remove 'privileged: true'."
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: false
            # Если есть initContainers, проверяем и их
            initContainers:
              - securityContext:
                  privileged: false
            # ephemeral containers тоже
            ephemeralContainers:
              - securityContext:
                  privileged: false

Что здесь происходит:
  • validationFailureAction: Enforce - блокируем создание/обновление, если правило нарушено.
  • background: true - фоновая проверка существующих подов (будет видно в PolicyReport).
  • pattern - описывает ожидаемую структуру. Поле privileged должно быть false. Если оно отсутствует или равно true, правило не выполняется.
  • message - текст ошибки, который увидит пользователь при kubectl apply.
Но этот вариант требует, чтобы securityContext существовал и privileged был явно задан. Если разработчик не указал securityContext вообще, поле privileged отсутствует, и pattern не сработает (потому что он ищет поле со значением false). В Kyverno можно использовать pattern с != (не равно) или deny условия.

Усовершенствованный вариант с запретом любого под, где privileged отсутствует или равен true:

YAML:
- name: privileged-containers
  match:
    any:
    - resources:
        kinds:
          - Pod
  validate:
    message: "Privileged containers are not allowed. Either set privileged: false or omit securityContext."
    pattern:
      spec:
        containers:
          - securityContext:
              privileged: false
        initContainers:
          - securityContext:
              privileged: false
        ephemeralContainers:
          - securityContext:
              privileged: false
    anyPattern:   # альтернативный паттерн: разрешаем, если securityContext отсутствует
      - spec:
          containers:
            - securityContext: null   # или отсутствует
          initContainers:
            - securityContext: null
          ephemeralContainers:
            - securityContext: null

Но anyPattern работает как «или», поэтому если хотя бы один контейнер имеет privileged: true, всё равно провалится? Нет, anyPattern проверяет все контейнеры. Если один контейнер соответствует первому паттерну (privileged=false), а другой второму (securityContext null), то всё равно нужен один паттерн, который подходит всем контейнерам одновременно. Так что anyPattern здесь не идеален.

Лучше использовать deny с условием, которое проверяет, что ни один контейнер не имеет privileged: true. В Kyverno 1.8+ есть операторы preconditions и deny.

Вариант с deny:

YAML:
- name: deny-privileged
  match:
    any:
    - resources:
        kinds:
          - Pod
  preconditions:
    all:
    - key: "{{ request.operation }}"
      operator: In
      value:
      - CREATE
      - UPDATE
  validate:
    message: "Privileged containers are not allowed."
    deny:
      conditions:
        any:
        - key: "{{ request.object.spec.containers[*].securityContext.privileged || 'false' }}"
          operator: Equals
          value: true
        - key: "{{ request.object.spec.initContainers[*].securityContext.privileged || 'false' }}"
          operator: Equals
          value: true
        - key: "{{ request.object.spec.ephemeralContainers[*].securityContext.privileged || 'false' }}"
          operator: Equals
          value: true

Но это сложновато. На практике я использую более простой подход: требовать явное указание securityContext с privileged: false через pattern + исключаем случаи, когда securityContext отсутствует, с помощью deny:

YAML:
- name: require-privileged-false
  match:
    any:
    - resources:
        kinds:
          - Pod
  validate:
    message: "SecurityContext must be defined with privileged=false for all containers."
    anyPattern:
    - spec:
        containers:
        - securityContext:
            privileged: false
        initContainers:
        - securityContext:
            privileged: false
        ephemeralContainers:
        - securityContext:
            privileged: false
    - spec:
        containers:
        - securityContext: null
        initContainers:
        - securityContext: null
        ephemeralContainers:
        - securityContext: null

Этот вариант разрешает либо явное privileged: false, либо полное отсутствие securityContext. Он не блокирует поды без securityContext. Если хочешь полностью запретить даже без securityContext, убирай второй паттерн.

Теперь про hostPID, hostNetwork, hostIPC и hostPorts. Это тоже опасные вещи. Политика:

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-host-namespaces
spec:
  validationFailureAction: Enforce
  rules:
    - name: host-namespaces
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: "Using host namespaces is prohibited."
        pattern:
          spec:
            hostPID: false
            hostNetwork: false
            hostIPC: false
    - name: host-ports
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: "Using host ports is prohibited."
        pattern:
          spec:
            containers:
              - ports:
                  - hostPort: null

3.2. Обязательные resource requests/limits для всех контейнеров​

Это классика. Без requests и limits твой кластер превращается в «свалку» - один под может сожрать всю память, а kube-scheduler не может нормально размещать поды. Заставим разработчиков указывать ресурсы.

Политика require-resource-limits.yaml:

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
  annotations:
    policies.kyverno.io/title: Require Resource Limits
spec:
  validationFailureAction: Enforce
  rules:
    - name: validate-resources
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: "Each container must specify cpu and memory requests and limits."
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    memory: "?*"
                    cpu: "?*"
                  limits:
                    memory: "?*"
                    cpu: "?*"
            initContainers:
              - resources:
                  requests:
                    memory: "?*"
                    cpu: "?*"
                  limits:
                    memory: "?*"
                    cpu: "?*"

"?*" означает «должно присутствовать и быть непустым». Но учти, что requests и limits могут быть указаны только в контейнере, а не в поде. Однако эта политика проверяет каждый контейнер.

Что если у тебя есть поды с ephemeralContainers? Их тоже стоит проверить, но они не всегда обязательны. Можно добавить ещё одно правило или расширить anyPattern.

Также имей в виду, что requests и limits можно указывать в виде строк или чисел. Kyverno понимает оба формата.

Если ты используешь LimitRange в namespace, то некоторые поды могут не иметь requests, но получат дефолтные. Однако лучше требовать явное указание.

3.3. Запрет тега​

Образы с тегом :latest - это бомба замедленного действия. Ты никогда не знаешь, какой именно образ будет подтянут при перезапуске. Воспроизводимость сборок летит к чёрту. Давайте запретим.

Политика disallow-latest-tag.yaml:

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: "Using ':latest' tag is not allowed. Specify an explicit tag or use digest."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
            initContainers:
              - image: "!*:latest"
            ephemeralContainers:
              - image: "!*:latest"

"!*:latest" означает «не соответствует шаблону, оканчивающемуся на :latest». Но это разрешает теги вроде v1.2.3 и также image@sha256:... (digest). Однако если разработчик напишет просто nginx (без тега), Kubernetes подставит :latest, и политика его пропустит? Нет, потому что nginx не содержит :latest, но по факту будет использован nginx:latest. Это проблема: политика проверяет строку, которую ввёл пользователь. Если пользователь не указал тег, то это nginx - он не заканчивается на :latest, поэтому пропускается. А потом kubelet подтянет nginx:latest.

Чтобы это исправить, нужно либо требовать явный тег или digest, либо мутировать отсутствующий тег в конкретный (например, ставить :stable). Лучше комбинировать: валидация запрещает :latest и требует, чтобы либо был тег (кроме latest), либо digest.

Более строгая политика:

YAML:
- name: require-image-digest-or-tag
  validate:
    message: "Image must use a digest (e.g., @sha256:...) or a tag other than 'latest'."
    pattern:
      spec:
        containers:
          - image: "?*"
            # Проверка, что image либо содержит '@' (digest), либо имеет тег, не равный latest
            # Используем JMESPath в preconditions, но можно и так:
    deny:
      conditions:
        any:
        - key: "{{ regex_match('.*:latest$', request.object.spec.containers[*].image) }}"
          operator: Equals
          value: true
        - key: "{{ regex_match('^[^@]+$', request.object.spec.containers[*].image) && !regex_match('.*:.*', request.object.spec.containers[*].image) }}"
          operator: Equals
          value: true

Этот вариант сложен. Проще использовать мутацию: если разработчик не указал тег, мутировать в :stable или конкретный digest, а валидацией запретить :latest. Но мутация - это следующий раздел.

Когда базовые validate-политики уже на месте, полезно посмотреть на этот уровень шире - не только как на набор YAML-ограничений, а как на часть общей модели защиты кластера. В недавней статье: "LOTL в мире контейнеров: уязвимости и защиты" мы рассказали, как admission-контроль, Pod Security, сетевые политики и runtime-наблюдение складываются в одну практическую схему, где Kyverno - не отдельный артефакт, а часть рабочего контура безопасности.

4. Mutate и Generate политики: когда недостаточно просто запретить​

Иногда просто запретить - недостаточно. Разработчики всё равно будут забывать про securityContext или labels. Им нужна помощь: автодополнение. Mutate политики позволяют добавлять или изменять поля в объектах до того, как они попадут в кластер. Generate политики позволяют создавать новые объекты (например, NetworkPolicy) при создании namespace.

4.1. Auto-injection: добавление securityContext в pods без него​

Допустим, ты хочешь, чтобы каждый под имел securityContext с runAsNonRoot: true и runAsUser: 1000. Но разработчики ленятся. Давай добавим это автоматически, если поле отсутствует.

Mutate политика add-security-context.yaml:

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-security-context
spec:
  rules:
    - name: add-security-context
      match:
        any:
        - resources:
            kinds:
              - Pod
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              - (name): "*"
                securityContext:
                  +(runAsNonRoot): true
                  +(runAsUser): 1000
                  +(capabilities):
                    +(drop):
                      - ALL
            initContainers:
              - (name): "*"
                securityContext:
                  +(runAsNonRoot): true
                  +(runAsUser): 1000
                  +(capabilities):
                    +(drop):
                      - ALL

Здесь используется patchStrategicMerge. Конструкция (name): "*" означает «для всех контейнеров, у которых есть поле name». Мы добавляем securityContext, если его нет. + означает «добавить, если отсутствует». Это безопасно: если разработчик уже указал securityContext, мы не перезаписываем его поля, а только добавляем недостающие.

Но важно: если разработчик уже указал securityContext, но без runAsNonRoot, то мы добавим runAsNonRoot: true. Если он указал runAsNonRoot: false, то поле уже есть, и мы не перезаписываем, потому что используем +. Это может привести к несоответствию, если разработчик сознательно хочет runAsNonRoot: false. В таком случае лучше добавить валидацию, которая запрещает явный runAsNonRoot: false. Или использовать overwrite вместо +.

Можно также мутировать imagePullPolicy, добавлять метки и аннотации.

Добавление default меток:

YAML:
mutate:
  patchStrategicMerge:
    metadata:
      labels:
        +(managed-by): kyverno
        +(environment): production

4.2. Default labels: автоматическое добавление team/env/cost-center labels​

Это часто требуется для отслеживания затрат или организации. Ты хочешь, чтобы на каждый новый под, деплоймент, сервис ставились определённые метки. Но их могут забыть. Можно мутировать не только поды, но и другие ресурсы.

Политика для добавления меток ко всем ресурсам (с использованием match):

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: inject-labels
spec:
  rules:
    - name: add-labels
      match:
        any:
        - resources:
            kinds:
              - Deployment
              - StatefulSet
              - DaemonSet
              - Service
              - ConfigMap
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              +(team): platform
              +(cost-center): "12345"

Но будь осторожен: если ты применишь это к Deployment, то лейблы появятся только на самом Deployment, а не на создаваемых подах. Чтобы добавить лейблы в поды, нужно мутировать template.metadata.labels.

Для подов внутри Deployment:

YAML:
- name: add-labels-to-pod-template
  match:
    any:
    - resources:
        kinds:
          - Deployment
  mutate:
    patchStrategicMerge:
      spec:
        template:
          metadata:
            labels:
              +(team): platform

Или ты можешь сделать одну политику, которая добавляет метки на любой объект, содержащий spec.template.metadata.labels. Но это уже сложнее.

4.3. Auto-generate: создание NetworkPolicy при создании нового namespace​

Generate политики - это мощный инструмент для создания сопутствующих ресурсов. Например, ты хочешь, чтобы каждый новый namespace автоматически получал дефолтную NetworkPolicy, запрещающую весь входящий трафик, или ResourceQuota.

Политика generate-networkpolicy.yaml:

YAML:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-networkpolicy
spec:
  rules:
    - name: generate-networkpolicy
      match:
        any:
        - resources:
            kinds:
              - Namespace
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: deny-all-ingress
        namespace: "{{ request.object.metadata.name }}"
        synchronize: true   # если true, то при удалении NetworkPolicy Kyverno восстановит её
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress

После создания namespace Kyverno создаст NetworkPolicy с именем deny-all-ingress, которая запрещает весь входящий трафик к подам в этом namespace (если нет более разрешающих политик). synchronize: true означает, что если кто-то удалит эту NetworkPolicy, Kyverno восстановит её.

Generate для ResourceQuota:

YAML:
generate:
  apiVersion: v1
  kind: ResourceQuota
  name: default-quota
  namespace: "{{ request.object.metadata.name }}"
  synchronize: true
  data:
    spec:
      hard:
        requests.cpu: "4"
        requests.memory: 8Gi
        limits.cpu: "8"
        limits.memory: 16Gi
        persistentvolumeclaims: "10"

Это гарантирует, что в каждом новом namespace будут установлены квоты.


5. Мониторинг и compliance: как жить с политиками и не сойти с ума​

Внедрить политики - полдела. Нужно ещё понимать, что происходит в кластере, какие объекты нарушают правила, и как это всё отслеживать. Kyverno предоставляет для этого PolicyReport CRD, а также интеграцию с дашбордами.

5.1. PolicyReport CRD: структурированные отчёты о нарушениях​

Kyverno генерирует объекты PolicyReport для каждого неймспейса и ClusterPolicyReport для кластерных ресурсов. Эти отчёты содержат список нарушений (violations) по каждой политике.

Посмотреть отчёты можно через kubectl:

Bash:
kubectl get policyreport -A
kubectl get clusterpolicyreport

Структура PolicyReport:

YAML:
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
  name: kyverno-report
  namespace: default
results:
  - policy: disallow-privileged-containers
    rule: privileged-containers
    resources:
      - kind: Pod
        name: bad-pod
        namespace: default
    message: "Privileged containers are not allowed."
    status: Fail
    timestamp:
      seconds: 1700000000
summary:
  fail: 1
  pass: 4
  skip: 0
  warn: 0

Ты можешь использовать эти отчёты для автоматического мониторинга, например, через Prometheus (экспортер Kyverno собирает метрики), или через интеграцию с другими инструментами.

Совет: не держи тысячи старых PolicyReport. Kyverno автоматически удаляет отчёты для удалённых ресурсов, но если у тебя много нарушений, количество отчётов может расти. Включи cleanup контроллер для периодической очистки.

5.2. Kyverno Dashboard (Policy Reporter UI) - визуализация compliance​

Для человеко-читаемого интерфейса существует проект Policy Reporter (GitHub - kyverno/policy-reporter: Monitoring and Observability Tool for the PolicyReport CRD with an optional UI.). Он собирает PolicyReport со всего кластера, отображает их в веб-интерфейсе, позволяет фильтровать, экспортировать в PDF, интегрироваться с SIEM.

Установка через Helm:

Bash:
helm repo add policy-reporter https://kyverno.github.io/policy-reporter
helm upgrade --install policy-reporter policy-reporter/policy-reporter \
  --namespace policy-reporter --create-namespace

После установки открой доступ к сервису (через NodePort или Ingress) и получишь красивый дашборд с графиками, списком нарушений и возможностью просмотреть, какие именно ресурсы нарушают какие политики.

Это особенно полезно, когда нужно показать руководству «мы заблокировали X нарушений за неделю».

5.3. Интеграция с CI/CD: kyverno apply --cluster для pre-deployment проверки​

Если ты используешь CI/CD (GitLab CI, Jenkins, GitHub Actions), ты можешь проверять манифесты на соответствие политикам ещё до того, как они попадут в кластер. Kyverno предоставляет CLI-команду kyverno apply, которая имитирует обработку политик на локальных файлах.

Пример проверки:

Bash:
kyverno apply /path/to/manifests \
  --policy /path/to/policies \
  --resource /path/to/resources \
  --cluster  # если политики кластерные

Эта команда выведет результат в консоль и вернёт ненулевой код, если есть нарушения. Ты можешь встроить её в пайплайн:

YAML:
# .gitlab-ci.yml
test-policies:
  stage: test
  image: kyverno/kyverno-cli:latest
  script:
    - kyverno apply ./k8s/ --policy ./kyverno-policies/ --cluster
  only:
    - merge_requests

Так ты не допустишь в мастер манифесты, которые не проходят валидацию. Это сдвигает безопасность влево (shift left).


Сравнение Kyverno vs OPA/Gatekeeper: почему я выбрал YAML​

Я не могу не затронуть эту тему. Если ты работал с Open Policy Agent (OPA) и Gatekeeper, ты знаешь, что там всё строится на Rego - языке, который сам по себе требует изучения. Да, Rego мощный, но его синтаксис напоминает Prolog, и для большинства простых политик (как запрет privileged) ты пишешь многословные конструкции.

Kyverno:
  • Политики на YAML, который ты и так пишешь каждый день.
  • Низкий порог входа.
  • Встроенные функции для патчей, генерации, мутации.
  • Background scanning из коробки.
OPA/Gatekeeper:
  • Rego даёт больше гибкости для сложных логических условий, особенно если нужно пересекать данные из разных источников.
  • Более зрелая экосистема, интеграция с многими системами.
  • Возможность использовать внешние данные (data.inventory) для более сложных решений.
Производительность: оба инструмента работают как вебхуки. Kyverno быстрее на простых проверках, потому что не требует компиляции Rego, но при сложных шаблонах и множестве политик может отставать. В production я использую Kyverno для 80% политик (валидация, мутация, генерация) и Gatekeeper для оставшихся 20% сложных политик (например, проверка, что образ взят только из доверенного реестра, и подпись подписана). Но можно обойтись и одним Kyverno, если не злоупотреблять сложной логикой.


Заключение: что мы получили​

Мы развернули Kyverno в production с настройками HA, подготовили политики валидации, которые не пропускают привилегированные контейнеры, образы с :latest и поды без ресурсов. Настроили автоматическое добавление securityContext, меток и генерацию NetworkPolicy для новых неймспейсов. Всё это контролируется через PolicyReport и визуализируется в дашборде.

Теперь твой кластер не просто «рабочий», он ещё и безопасный. Разработчики могут писать манифесты как раньше, но Kyverno будет их поправлять и не давать делать глупости. А если кто-то попытается обойти политики - получит понятное сообщение об ошибке.

Конечно, это только начало. Политик может быть десятки: запрет на использование определённых образов, проверка наличия liveness/readiness probes, требование аннотаций для сервисов, ограничение на порты NodePort, блокировка определённых API-версий. Но теперь у тебя есть фундамент.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab