Стадия жизненного цикла модуля: Экспериментальная версия
У модуля есть требования для установки
ClusterSecurityEventConfig с Loki (явный список источников)
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
name: default
spec:
defaultSeverityThreshold: High
enabledSources:
- clusterSecurityEventShipper/kube-audit/kube-apiserver
destinations:
- cluster-lokiClusterSecurityEventConfig с масками
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventConfig
metadata:
name: default
spec:
defaultSeverityThreshold: High
enabledSourcesMasks:
- clusterSecurityEventShipper/kube-audit/*
- podSecurityEventShipper/*
destinations:
- cluster-lokiClusterSecurityEventDestination (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: truePodSecurityEventShipper (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: levelClusterSecurityEventShipper (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: trueClusterSecurityEventShipper (KubernetesPods по выбранным неймспейсам)
Кластерный шиппер по умолчанию собирает логи подов из всех неймспейсов. Чтобы сузить набор,
перечислите неймспейсы. Оба списка можно задать одновременно, исключение применяется поверх
включения: здесь шиппер охватывает prod-api и prod-worker и тут же исключает prod-worker,
поэтому остаётся только prod-api.
apiVersion: security.deckhouse.io/v1alpha1
kind: ClusterSecurityEventShipper
metadata:
name: app-audit
spec:
- source: app-audit
input:
type: KubernetesPods
kubernetesPods:
namespaceSelector:
matchNames:
- prod-api
- prod-worker
excludeNames:
- prod-worker
labelSelector:
matchLabels:
app: audit
parser:
- name: audit
parser:
type: JSON
produces:
- eventCode: APP_LOGIN_FAILED
extract:
field: message
operator: Regex
values:
- '.*"event":"login_failed".*'Оба списка транслируются в условие на метку kubernetes.io/metadata.name, которую несёт каждый
неймспейс. Имя должно быть корректной DNS-1123-меткой, иначе объект отклоняется.
Не задавайте namespaceSelector, чтобы собирать события со всех неймспейсов. Поле labelSelector
обязательно в обоих случаях и должно содержать matchLabels или matchExpressions — пустой
селектор отклоняется.
Несколько приёмников (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-prodSecurityEventLoggingTransformationRules (переиспользуемый парсер)
На эти правила ссылается шиппер через parserRef, и объект должен лежать в неймспейсе самого
шиппера. Какие поды читать, решает шиппер своим полем input.kubernetesPods.labelSelector; поле
spec.selector ниже требуется схемой и для сопоставления не используется.
apiVersion: security.deckhouse.io/v1alpha1
kind: SecurityEventLoggingTransformationRules
metadata:
name: falco
namespace: d8-runtime-audit-engine
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:
# Input KubernetesPods кладёт имя пода и его неймспейс в корень события,
# поэтому оба пути записаны с префиксом `@root.`.
enrich:
- target: actor.id
source: Plugin
plugin: k8s-pod-info
value: serviceAccountName
args:
- key: pod_name
value: '@root.pod'
- key: namespace
value: '@root.namespace'
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: d8-runtime-audit-engine
spec:
- source: runtime-audit-engine
input:
type: KubernetesPods
kubernetesPods:
labelSelector:
matchLabels:
app: runtime-audit-engine
parser:
- name: falco
parser:
type: JSON
producesDefaults:
# Falco записывает ключи `output_fields` плоскими, вместе с точками, поэтому ключ взят в кавычки.
enrich:
- target: actor.id
source: Plugin
plugin: k8s-container-info
value: serviceAccountName
args:
- key: container_id
value: output_fields["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: runtime-audit-engine
parser:
- name: falco
parser:
type: JSON
producesDefaults:
# Falco записывает ключи `output_fields` плоскими, вместе с точками, поэтому ключ взят в кавычки.
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: runtime-audit-engine
parser:
- name: falco
parser:
type: JSON
producesDefaults:
# Ключ взят в кавычки: парсер кладёт его в `output_fields` плоским, вместе с точками.
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, доверив сглаживание окну самого запроса. severityLevel4 и ниже блокирует обновление 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: LowSocket-приёмник (сырой 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: UDPSocket-приёмник (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