Стадия жизненного цикла модуля: Экспериментальная версия

У модуля есть требования для установки

Перед включением модуля необходимо разрешить использование экспериментальных модулей в ModuleConfig deckhouse. Для этого установите параметр allowExperimentalModules в true.

ClusterSecurityEventConfig с Loki (явный список источников)

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
  name: default
spec:
  defaultSeverityThreshold: High
  enabledSources:
    - clusterSecurityEventShipper/kube-audit/kube-apiserver
  destinations:
    - cluster-loki

ClusterSecurityEventConfig с масками

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
  name: default
spec:
  defaultSeverityThreshold: High
  enabledSourcesMasks:
    - clusterSecurityEventShipper/kube-audit/*
    - podSecurityEventShipper/*
  destinations:
    - cluster-loki

ClusterSecurityEventDestination (Loki)

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: cluster-loki
spec:
  type: Loki
  loki:
    endpoint: https://loki.d8-monitoring:3100
    auth:
      strategy: Bearer
      token: EXAMPLE
    tls:
      verifyCertificate: false
      verifyHostname: false

Учётные данные (token, password) также можно подключать из Secret через tokenSecretRef/passwordSecretRef вместо указания inline. Secret должен находиться в неймспейсе d8-security-events-manager с ключом value в поле data и меткой security-events-manager.deckhouse.io/credential-secret: "true". Inline-поля учётных данных и их *SecretRef-аналоги взаимно исключают друг друга.

SecurityEventDefinition

apiVersion: security.deckhouse.io/v1alpha1
kind: SecurityEventDefinition
metadata:
  name: k8s-privilege-escalation
spec:
  code: K8S_PRIV_ESC
  category: Rbac
  severity: High
  description: "Попытка создать привилегированный под или повысить права"
  source: kube-apiserver
  fields:
    - name: metadata.extra.privileges
      required: true

PodSecurityEventShipper (KubernetesPods с inline parser)

apiVersion: security.deckhouse.io/v1alpha1
kind: PodSecurityEventShipper
metadata:
  name: my-audit
  namespace: my-namespace
spec:
  - source: my-audit-app
    input:
      type: KubernetesPods
      kubernetesPods:
        labelSelector:
          matchLabels:
            app: audit
    parser:
      - name: app
        parser:
          type: Regex
          regex:
            patterns:
              - '^(?P<level>\w+)\s+(?P<msg>.+)$'
        fields:
          - name: level
            type: String
    produces:
      - eventCode: K8S_PRIV_ESC
        extract:
          field: message
          operator: Regex
          values:
            - '.*'
        transform:
          - key: event.severity
            value: level

ClusterSecurityEventShipper (File с parserRef)

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventShipper
metadata:
  name: kube-audit
spec:
  - source: kube-apiserver
    input:
      type: File
      files:
        - /var/log/kube-apiserver/audit.log
    parserRef: audit-json
    produces:
      - eventCode: K8S_AUDIT_FAIL

С ClusterSecurityEventLoggingTransformationRules:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventLoggingTransformationRules
metadata:
  name: audit-json
spec:
  type: File
  file:
    paths:
      - /var/log/kube-apiserver/audit.log
    transform:
      parser:
        type: JSON
      fields:
        - name: stage
          type: String
        - name: responseStatus
          type: Int
      drop_raw: true

Несколько приёмников (Loki + Splunk)

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: cluster-loki
spec:
  type: Loki
  loki:
    endpoint: https://loki.example:3100
---
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: splunk-prod
spec:
  type: SplunkHEC
  splunkHEC:
    endpoint: https://splunk.example:8088
    token: YOUR_TOKEN
---
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
  name: default
spec:
  defaultSeverityThreshold: Medium
  enabledSourcesMasks:
    - "*"
  destinations:
    - cluster-loki
    - splunk-prod

SecurityEventLoggingTransformationRules (переиспользуемый парсер)

apiVersion: security.deckhouse.io/v1alpha1
kind: SecurityEventLoggingTransformationRules
metadata:
  name: falco
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: runtime-audit-engine
  containers:
    - name: falco
      parser:
        type: JSON
      fields:
        - name: priority
          type: String
        - name: output
          type: String
      drop_raw: true

Обогащение через плагин (k8s-pod-info)

С помощью плагинов можно обогащать события данными, которых нет в исходном логе. Описание плагинов (обязательные входные параметры, доступные выходные значения и шаблон элемента массива enrich) приведено в разделе Плагины обогащения.

apiVersion: security.deckhouse.io/v1alpha1
kind: PodSecurityEventShipper
metadata:
  name: app-audit
  namespace: my-namespace
spec:
  - source: my-audit-app
    input:
      type: KubernetesPods
      kubernetesPods:
        labelSelector:
          matchLabels:
            app: audit
    parser:
      - name: app
        parser:
          type: JSON
    producesDefaults:
      # k8s.pod.name / k8s.namespace.name заполняются input-ом KubernetesPods.
      enrich:
        - target: actor.id
          source: Plugin
          plugin: k8s-pod-info
          value: serviceAccountName
          args:
            - key: pod_name
              value: k8s.pod.name
            - key: namespace
              value: k8s.namespace.name
    produces:
      - eventCode: K8S_PRIV_ESC
        extract:
          field: message
          operator: Regex
          values:
            - '.*'

Режим поиска по container ID

Если в исходном событии есть только container runtime ID (характерно для syscall/falco событий), используйте плагин k8s-container-info, чтобы определить serviceAccountName, name или namespace за один поиск:

apiVersion: security.deckhouse.io/v1alpha1
kind: PodSecurityEventShipper
metadata:
  name: falco-audit
  namespace: kube-system
spec:
  - source: falco
    input:
      type: KubernetesPods
      kubernetesPods:
        labelSelector:
          matchLabels:
            app: falco
    parser:
      - name: falco
        parser:
          type: JSON
    producesDefaults:
      # container.id заполняется парсером из JSON-вывода falco.
      enrich:
        - target: actor.id
          source: Plugin
          plugin: k8s-container-info
          value: serviceAccountName
          args:
            - key: container_id
              value: container.id
    produces:
      - eventCode: K8S_PRIV_ESC
        extract:
          field: message
          operator: Regex
          values:
            - '.*'

Обогащение через плагин (k8s-nodeuser-info)

Shipper может определить имя статического пользователя (metadata.name объекта nodeusers.deckhouse.io) по системному UID, присутствующему в событии, используя enrich.source: Plugin с плагином k8s-nodeuser-info. Описание плагина (обязательные входные параметры, доступные выходные значения и шаблон элемента массива enrich) приведено в разделе Плагины обогащения.

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventShipper
metadata:
  name: falco-nodeuser-enrich
spec:
  - source: runtime-audit-engine
    input:
      type: KubernetesPods
      kubernetesPods:
        labelSelector:
          matchLabels:
            app: falco
    parser:
      - name: falco
        parser:
          type: JSON
    producesDefaults:
      # output_fields.user.uid заполняется парсером из JSON-вывода falco.
      enrich:
        - target: actor.name
          source: Plugin
          plugin: k8s-nodeuser-info
          value: username
          args:
            - key: uid
              value: output_fields.user.uid
    produces:
      - eventCode: K8S_SSH_LOGIN
        extract:
          field: message
          operator: Regex
          values:
            - '.*'

Обогащение через пользовательский External-плагин

Пользователи могут создавать External-ресурсы ClusterSecurityEventEnrichmentPlugin для регистрации собственных enrichment-эндпоинтов. Ниже приведён пример External-плагина, который определяет информацию о сотруднике по badge ID.

Сначала создайте ресурс CSEP:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventEnrichmentPlugin
metadata:
  name: employee-info
spec:
  type: External
  description: "Resolve employee info by badge ID"
  endpoint:
    url: http://employee-enricher.hr-namespace.svc:8080/lookup
  args:
    - name: badge_id
      required: true
      description: "Event field path containing the employee badge ID"
  returns:
    fields:
      - name: username
        type: String
        description: "Employee username"
      - name: department
        type: String
        description: "Employee department"

Затем ссылайтесь на него в enrich-правиле shipper-а:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventShipper
metadata:
  name: badge-enrich-example
spec:
  - source: runtime-audit-engine
    input:
      type: KubernetesPods
      kubernetesPods:
        labelSelector:
          matchLabels:
            app: falco
    parser:
      - name: falco
        parser:
          type: JSON
    producesDefaults:
      # output_fields.user.badge_id заполняется парсером.
      enrich:
        - target: actor.name
          source: Plugin
          plugin: employee-info
          value: username
          args:
            - key: badge_id
              value: output_fields.user.badge_id
    produces:
      - eventCode: K8S_BADGE_LOGIN
        extract:
          field: message
          operator: Regex
          values:
            - '.*'

Конфигурация буфера

Production-настройка: Disk + Block (по умолчанию)

Настройки буфера по умолчанию предназначены для production — дисковый буфер с поведением Block при переполнении гарантирует нулевые потери событий безопасности при временной недоступности назначений.

Конфигурация модуля (ModuleConfig):

apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
  name: security-events-manager
spec:
  version: 1
  settings:
    gateway:
      buffer:
        type: Disk
        whenFull: Block
        maxSize: 512Mi
        maxEvents: 500
      logShipperBuffer:
        type: Disk
        whenFull: Block
        maxSize: 257Mi    # на узел; должно быть > 256Mi — минимум дискового буфера log-shipper
        maxEvents: 500

Переопределение конкретных параметров в ClusterSecurityEventDestination (например, больший буфер для удалённого Elasticsearch):

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: remote-elasticsearch
spec:
  type: Elasticsearch
  elasticsearch:
    endpoint: https://es.example.com:9200
    index: security-events
    auth:
      strategy: Bearer
      tokenSecretRef:
        name: es-token
  buffer:
    type: Disk
    whenFull: Block
    maxSize: 1Gi    # Больший буфер для ненадёжного удалённого назначения.
    maxEvents: 500

Тестовая настройка: Memory + DropNewest

Для тестовых/dev-окружений, где потеря данных допустима и требуется максимальная скорость без обратного давления:

Конфигурация модуля:

apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
  name: security-events-manager
spec:
  version: 1
  settings:
    gateway:
      buffer:
        type: Memory
        whenFull: DropNewest
        maxSize: 50Mi
        maxEvents: 500
      logShipperBuffer:
        type: Memory
        whenFull: DropNewest
        maxSize: 50Mi
        maxEvents: 500

Алертинг по событиям безопасности

ClusterSecurityEventAlertRule ищет закономерность в собранных событиях безопасности и вызывает алерт Prometheus, как только закономерность обнаружена. Например: «один инициатор прочитал больше 20 секретов за 5 минут» или «субъекту выдали роль cluster-admin».

Закономерность описывается запросом на LogQL, языке запросов для поиска логов в Loki, и вычисляется по расписанию относительно Loki кластера. Поэтому для работы любого правила должен быть включён destination cluster-loki: параметр securityEventConfig.destinations должен содержать cluster-loki, как описано в справочнике по конфигурации модуля. Если destination отсутствует или его тип отличается от Loki, каждое правило помечается Degraded в status.conditions и ни одно правило не вычисляется.

Сработавшее правило вызывает алерт D8SecurityEventAlertFiring — обычный алерт Prometheus. Он становится объектом ClusterAlert, виден в консоли вместе с остальными кластерными алертами и попадает во все каналы, куда кластер уже доставляет алерты. Доставку настраивать не требуется, а исчезает алерт самостоятельно через 5–7 минут после того, как правило перестало совпадать. Чтобы получить список созданных объектов, выполните команду:

d8 k get clusteralerts

Из чего состоит правило

Правила размещаются в именованных группах. Группа только организует правила и не влияет на то, как правило вычисляется. У каждого правила в группе есть следующие поля:

  • alert — короткое имя того, что оно обнаруживает, например MassSecretAccess.
  • mode — в какой из двух форм записано правило: Expr для LogQL-запроса, написанного вручную, Match для структурированной формы. Поле обязательное и обязано совпадать с заполненным блоком. Оно явно указывает то, что и так следует из наличия expr или match, чтобы и читатель манифеста, и веб-интерфейс платформы понимали форму правила, не разбирая, какой блок заполнен.
  • expr или match — какие события учитывает правило и когда оно срабатывает. У каждого правила задано ровно одно из двух, названное в mode:
    • expr — LogQL-запрос, определяющий, срабатывает ли правило. Он должен быть запросом метрики, то есть возвращать число, и включать само сравнение, например count_over_time({source_component="kube-audit"}[5m]) > 20. Условие срабатывания больше ничем не задаётся: достижение порога, указанного в запросе, и есть срабатывание. Пример приведён ниже.
    • match с необязательным aggregation — то же условие, собранное из структурированных полей: какие коды событий, источники, категории и инициаторы учитываются и, если задан aggregation, сколько их в пределах какого окна вызывает срабатывание. Эта форма компилируется в тот же LogQL-запрос, который мог бы быть записан в expr, то есть является способом составить такой запрос, а не отдельным путём вычисления. Подробности приведены ниже в разделе о простой форме.
  • for — интервал, в течение которого expr должен непрерывно оставаться истинным, прежде чем правило сработает. Поле отсекает разовые всплески: с for: 5m всплеск, который длится пару минут и затем прекращается, не вызывает срабатывания правила. Значение по умолчанию — 0s: срабатывание происходит при первом совпадении. Ожидание pending и firing то же, что и у поля for у PrometheusRule.
  • annotations — текст, который несёт сработавший алерт; поле обязательное. В annotations.summary требуется указать, что именно обнаружило правило. Полная структура приведена в справочнике по кастомным ресурсам.
  • severityLevel и labels описаны в справочнике по кастомным ресурсам.

Структура повторяет PrometheusRule: именованные группы, у каждой параметр interval и список правил с полями alert, expr, for, labels и annotations, где вместо PromQL в expr используется LogQL.

Простейшее правило совпадает с одним кодом события и срабатывает, как только событие появляется:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: cluster-admin-bound
spec:
  groups:
    - name: default
      rules:
        - alert: ClusterAdminBound
          mode: Expr
          expr: |
            count_over_time({source_component="kube-audit", event_code="K8S_CLUSTER_ADMIN_BOUND"} [1m]) > 0
          severityLevel: 5
          annotations:
            summary: "Субъекту выдана роль cluster-admin"

Запрос состоит из следующих частей:

  • селектор {...} выбирает потоки логов, в которых идёт поиск;
  • count_over_time(...[1m]) считает подходящие строки за последнюю минуту;
  • > 0 — само условие.

Ничего сложнее не требуется, поскольку выдача роли cluster-admin — редкое событие, заслуживающее внимания при каждом появлении.

Оба поля в селекторе — лейблы стрима, поэтому Loki разрешает их по индексу и не читает исключённые потоки. | json требуется только полям, которые остаются в теле события. Какие поля относятся к каждой группе и почему фильтр по другую сторону этой границы обходится дорого, описано ниже в разделе об особенностях написания правил.

Подсчёт событий по инициатору

Большинство правил отвечают не на вопрос, произошло ли событие вообще, а на вопрос, не произошло ли оно слишком много раз для одного инициатора. В таких правилах применяется sum by (...), который ведёт отдельный счётчик для каждого значения поля, а не складывает все подходящие события вместе.

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: mass-secret-access
spec:
  groups:
    - name: default
      interval: 30s
      rules:
        - alert: MassSecretAccess
          mode: Expr
          # Акторы платформы читают секреты непрерывно — на простаивающем кластере
          # один ServiceAccount DKP даёт тысячи чтений в час, — поэтому правило про
          # людей и приложения исключает шаблон системных инициаторов, тот же, что
          # использует фильтр «Исключить системные» в веб-интерфейсе платформы.
          expr: |
            sum by (actor_id) (
              count_over_time(
                {source_component="kube-audit",
                 event_code="K8S_SECRET_ACCESSED",
                 actor_id!="",
                 actor_id!~`system:serviceaccount:kube-system:.*|system:serviceaccount:d8-.*|system:node:.*|system:kube-.*|system:apiserver|kubernetes-admin`}
                [5m])
            ) > 20
          for: 0m
          severityLevel: 6
          annotations:
            summary: "Один инициатор прочитал необычно много секретов"
            description: "Инициатор {{ $labels.actor_id }} прочитал секреты {{ $value }} раз за 5 минут. Проверьте, ожидаем ли такой доступ."

По сравнению с первым примером в этом правиле добавлено следующее:

  • матчер, исключающий известные платформенные личности (actor_id!~...), чтобы рутинная активность платформы не заглушала правило;
  • sum by (actor_id), который превращает один общий счётчик в отдельный счётчик на каждого инициатора, поэтому правило срабатывает, когда порог 20 превышает счётчик любого отдельного инициатора, а не суммарный счётчик по кластеру;
  • {{ $labels.actor_id }} и {{ $value }} в description, которые цитируют конкретного инициатора и счётчик, вызвавшие срабатывание. Это та же шаблонизация $labels и $value, которой Prometheus заполняет аннотации PrometheusRule.

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

Простая форма

Писать LogQL вручную не обязательно. Поле match выбирает события по структурированным полям, а необязательное поле aggregation считает их по группам и вызывает срабатывание, как только счётчик группы достигает threshold за window. Это та же форма, в которой правила алертинга модуля были устроены до LogQL, но теперь она компилируется в LogQL-запрос expr, а не запускает отдельный путь вычисления. Без aggregation правило на основе match срабатывает на каждое подходящее событие.

В следующем примере правило cluster-admin переписано через match:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: cluster-admin-bound
spec:
  groups:
    - name: default
      rules:
        - alert: ClusterAdminBound
          mode: Match
          match:
            eventCodes: ["K8S_CLUSTER_ADMIN_BOUND"]
          severityLevel: 5
          annotations:
            summary: "Субъекту выдана роль cluster-admin"

А пример про массовое чтение секретов — excludeSystemActors по умолчанию true, поэтому его даже не нужно писать явно — с группировкой и порогом через aggregation:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: mass-secret-access
spec:
  groups:
    - name: default
      interval: 30s
      rules:
        - alert: MassSecretAccess
          mode: Match
          match:
            eventCodes: ["K8S_SECRET_ACCESSED"]
          aggregation:
            groupBy: ["ActorID"]
            threshold: 20
            window: "5m"
          for: 0m
          severityLevel: 6
          annotations:
            summary: "Один инициатор прочитал необычно много секретов"
            description: "Инициатор {{ $labels.actor_id }} прочитал секреты {{ $value }} раз за 5 минут. Проверьте, ожидаем ли такой доступ."

Остальные поля match — sources, categories, severityMin, actorType, actors и excludeActors — покрывают то же, что и построчные фильтры expr. Каждое из них описано в справочнике по кастомным ресурсам. Используйте expr напрямую, когда правилу требуется то, что match выразить не может: поле, которое он не покрывает, или фильтр более специфичный, чем точное совпадение или совпадение по маске, например числовое сравнение по полю JSON.

Примеры настройки правил для разных событий

Пример настройки правила, вызывающего срабатывание алерта, если один инициатор выполняет exec в несколько подов:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: repeated-pod-exec
spec:
  groups:
    - name: default
      rules:
        - alert: RepeatedPodExec
          mode: Expr
          expr: |
            sum by (actor_id) (
              count_over_time({source_component="kube-audit", event_code="K8S_POD_EXEC_ATTACH", actor_id!=""} [10m])
            ) > 5
          severityLevel: 6
          annotations:
            summary: "Один инициатор открыл шеллы в нескольких подах"

Пример настройки правила, вызывающего срабатывание алерта в случае всплеска правок RBAC в одном неймспейсе (эскалация привилегий), с группировкой по паре инициатор–неймспейс:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: rbac-edit-burst
spec:
  groups:
    - name: default
      rules:
        - alert: RbacEditBurst
          mode: Expr
          expr: |
            sum by (actor_id, object_namespace) (
              count_over_time({source_component="kube-audit", event_code="K8S_RBAC_RESOURCES_MODIFIED"} | json [5m])
            ) > 5
          severityLevel: 5
          annotations:
            summary: "Несколько объектов RBAC изменены за короткое время"
            description: "Сверьте {{ $labels.actor_id }} и {{ $labels.object_namespace }} с тем, что планировалось."

Пример настройки правила, вызывающего срабатывание алерта при брутфорсе (повторяющиеся отказы с одного адреса). Здесь используйте группировку по адресу клиента, а не по инициатору, неавторизованный запрос не содержит информации об инициаторе:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: unauthorized-burst
spec:
  groups:
    - name: default
      rules:
        - alert: UnauthorizedBurst
          mode: Expr
          expr: |
            sum by (actor_sourceIP) (
              count_over_time({source_component="kube-audit", event_code="K8S_UNAUTHORIZED_REQUEST"} | json [5m])
            ) > 10
          severityLevel: 5
          annotations:
            summary: "Повторяющиеся неавторизованные запросы с одного адреса"

Пример настройки правила для события, которое достаточно редко встречается, чтобы одного случая хватило для внимания, сознательно с уровнем, который также блокирует обновления DKP:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: containerd-integrity
spec:
  groups:
    - name: default
      rules:
        - alert: ContainerdIntegrityViolation
          mode: Expr
          expr: |
            count_over_time({source_component="kube-audit", event_code="D8_CONTAINERD_INTEGRITY_VIOLATION"} [1m]) > 0
          severityLevel: 4     # Сознательно низкий: такое событие должно и обновление платформы остановить.
          annotations:
            summary: "На узле не пройдена проверка целостности containerd"

Пример настройки правила, вызывающего срабатывание алерта, если изменения в RBAC инициированы субъектами, отличными от ServiceAccount:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventAlertRule
metadata:
  name: person-touched-rbac
spec:
  groups:
    - name: default
      rules:
        - alert: PersonTouchedRbac
          mode: Expr
          expr: |
            count_over_time(
              {source_component="kube-audit",
               event_category="Rbac",
               actor_id!~`system:serviceaccount:.*`}
              [1m]
            ) > 0
          severityLevel: 5
          annotations:
            summary: "RBAC изменён личностью, не являющейся ServiceAccount"

Клиентский сертификат или внешняя личность тоже не ServiceAccount, так что это не совсем «человек». Чтобы следить за конкретной личностью вместо этого, укажите её напрямую:

          expr: |
            count_over_time(
              {source_component="kube-audit",
               event_code="K8S_SECRET_ACCESSED",
               actor_id=~`system:serviceaccount:my-app:.*|kubernetes-admin`}
              [1m]
            ) > 0

Особенности написания правил

При написании правил учитывайте следующие особенности:

  • Правило действует только на события, записываемые политикой аудита кластера. Политика DKP по умолчанию логирует list секретов, но не get конкретного секрета по имени, поэтому правило на K8S_SECRET_ACCESSED срабатывает на перечисление секретов, а не на чтение одного известного. Она же отбрасывает большую часть запросов от группы system:authenticated, так что действия людей попадают в поток событий только там, где их записывает явное правило: изменения в неймспейсах платформы, правки RBAC, exec/attach и тому подобное. Чтобы убедиться, что правило не работает, посмотрите политику: событие, которое не записали, не может стать алертом.
  • Акторы платформы по умолчанию не исключены. Собственные компоненты Deckhouse создают большую часть рутинной активности в потоке событий — на простаивающем кластере один платформенный ServiceAccount может давать тысячи чтений секретов в час, — поэтому правилу про людей и приложения нужен line filter, исключающий их, как в примерах выше. Регулярное выражение там то же самое, что использует фильтр «Исключить системные» в консоли, чтобы интерфейс и ваши правила оставались согласованы: system:serviceaccount:kube-system:.*|system:serviceaccount:d8-.*|system:node:.*|system:kube-.*|system:apiserver|kubernetes-admin.
  • Фильтруйте по лейблам стрима внутри {...}, а не после | json. Destination Loki публикует пять полей как лейблы стрима — source_component, event_code, event_severity, event_category и actor_id. Матчер по любому из них, поставленный в селектор, разрешается по индексу Loki, поэтому исключённые им потоки не читаются вообще. Тот же матчер после | json заставляет Loki сначала вычитать все строки окна и разобрать их как JSON, чтобы затем почти всё выбросить — и так на каждом правиле, на каждом интервале. Все остальные поля (object_namespace, object_type, actor_sourceIP, …) живут в теле события, и им | json действительно нужен — именно поэтому два примера выше его сохраняют: они группируют по полю тела. Форма match выбирает нужную сторону этой границы сама.
  • Ограничение по кардинальности защищает Prometheus от разгона правила. Группировка по полю с неограниченным набором значений (имя объекта, путь запроса) способна превратить одно правило в тысячи алертов. После 500 различных наборов лейблов для одного правила остальные отбрасываются — правило продолжает срабатывать для уже отслеживаемых групп, а само оно помечается CardinalityLimitExceeded в status.conditions, чтобы это было заметно.
  • {{ $value }} в аннотации умножает число серий в Prometheus. Отрендеренные summary и description едут лейблами firing-метрики, поэтому каждое новое значение — отдельная серия, которая затем живёт весь retention. Счётчик, меняющийся на каждом вычислении (обычное дело для sum by (...) (count_over_time(...))), даёт новую серию за интервал: при interval: 30s это до 120 серий в час на каждую группу, сверх самих групп. Предпочитайте {{ $labels.<имя> }} — он стабилен, пока существует группа, — а {{ $value }} оставьте правилам, где счётчик меняется редко.
  • Состояние вычисления хранится в памяти, как и у большинства компонентов внутри кластера. Перезапуск компонента, который вычисляет правила, сбрасывает отсчёт for каждого правила заново; правило, которое вот-вот должно было сработать, после перезапуска снова ждёт for. Отдельно стоит следить за for, превышающим фактический интервал перезапусков компонента: такое правило никогда не доходит до конца отсчёта и не срабатывает вовсе. Держите for заметно меньше этого интервала либо оставьте 0s, доверив сглаживание окну самого запроса.
  • severityLevel 4 и ниже блокирует обновление DKP на кластерах, где в модуле deckhouse включён update.blockOnAlerts. Для критичного события это может быть осознанным решением, для рядового — неожиданностью.
  • Все правила вызывают срабатывание алерта с одним именем — D8SecurityEventAlertFiring. Отличить срабатывание одного правила от другого позволяет пара лейблов: rule (имя объекта) и alert (собственное поле alert: правила). Ищите по ним, а не по имени алерта. Этим правило отличается от PrometheusRule, где собственное поле alert: каждого правила напрямую становится именем алерта Prometheus — здесь же это имя несёт лейбл, потому что все правила ClusterSecurityEventAlertRule питают один общий алерт D8SecurityEventAlertFiring.

HTTP-приёмник (вебхук или HTTP-коллектор)

Отправка событий безопасности POST-запросом. Режимы framing существуют потому, что приёмники по-разному интерпретируют набор событий: одни читают один JSON-массив на запрос, а большинство коллекторов — поток JSON-записей.

Пример настройки отправки событий безопасности из кластера DKP во внешнюю систему через HTTP:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: siem-http
spec:
  type: Http
  http:
    endpoint: "https://collector.example.com/api/ingest"
    framing: NewlineDelimited      # По одному JSON-объекту на строку (NDJSON).
    auth:
      strategy: Bearer
      tokenSecretRef:
        name: siem-http-token
        key: token
    tls:
      verifyCertificate: true
    headers:
      X-Scope-OrgID: security
---
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
  name: default
spec:
  destinations:
    - siem-http
  enabledSourcesMasks:
    - "clusterSecurityEventShipper/*"
    - "podSecurityEventShipper/*"
  defaultSeverityThreshold: Low

Не указывайте заголовок Authorization в headers вместе с auth — они конфликтуют, и такой приёмник будет отклонён на этапе admission.

Socket-приёмник (CEF over syslog в SIEM)

Отправка событий безопасности в формате CEF, обёрнутых в RFC5424 syslog, по TCP/TLS в SIEM-приёмник:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: siem-syslog
spec:
  type: Socket
  socket:
    address: "siem.example.com:6514"
    mode: TCP
    tls:
      verifyCertificate: true
    encoding:
      codec: CEF
      syslogWrapper: RFC5424
      cef:
        deviceVendor: MyCompany
        deviceProduct: k8s-security
---
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
  name: default
spec:
  destinations:
    - siem-syslog
  enabledSourcesMasks:
    - "clusterSecurityEventShipper/*"
    - "podSecurityEventShipper/*"
  defaultSeverityThreshold: Low

Socket-приёмник (сырой JSON по UDP)

Отправка событий в JSON по UDP (без гарантии доставки, без TLS):

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: udp-collector
spec:
  type: Socket
  socket:
    address: "collector.example.com:514"
    mode: UDP

Socket-приёмник (Unix-сокет для сайдкара SIEM-агента)

Отправка событий в локальный Unix-сокет (например, для сайдкара SIEM-агента):

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: sidecar-siem
spec:
  type: Socket
  socket:
    address: "/var/run/siem.sock"
    mode: Unix

Переопределение конкретных параметров:

apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventDestination
metadata:
  name: test-loki
spec:
  type: Loki
  loki:
    endpoint: http://loki:3100
  buffer:
    type: Memory
    whenFull: DropNewest
    maxSize: 50Mi
    maxEvents: 500