iac/infrastructure/security-audit/base/helmrelease-collector.yaml
emelinda 9cb62ea9f0 security-audit: nil-guards в transform/audit — удаление managedFields/annotations
У части событий requestObject есть, а metadata/annotations в нём nil —
statement падал с "expected pcommon.Map but got nil" и засорял лог
(error_mode: ignore, события не терялись).
2026-11-06 18:03:30 +03:00

238 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: audit-collector
namespace: security-audit
spec:
interval: 10m
timeout: 10m
chart:
spec:
chart: opentelemetry-collector-prod
version: "0.117.1"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
mode: daemonset
fullnameOverride: audit-collector
# Дефолт чарта 0.117.1 (appVersion 0.120.0), задан явно: contrib-сборка с filelog, transform,
# kafka exporter, file_storage, health_check, memory_limiter. Зеркало в registry контура — см. README.
image:
repository: cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib
tag: "0.121.0-amd64"
command:
name: otelcol-contrib
imagePullSecrets:
- name: regcred
# Только мастера: журнал аудита есть только там.
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
rollout:
rollingUpdate:
maxUnavailable: 1
# Доступ к Kubernetes API коллектору не нужен.
clusterRole:
create: false
serviceAccount:
create: true
name: audit-collector # bound SA роли Vault security-audit — если вернёмся на Vault Agent
presets:
logsCollection: {enabled: false}
hostMetrics: {enabled: false}
kubernetesAttributes: {enabled: false}
kubeletMetrics: {enabled: false}
kubernetesEvents: {enabled: false}
clusterMetrics: {enabled: false}
# В daemonset-режиме чарт вешает hostPort на OTLP/Jaeger/Zipkin: приёмники не нужны,
# а hostPort конфликтует с другими DaemonSet-коллекторами на тех же узлах.
ports:
otlp: {enabled: false}
otlp-http: {enabled: false}
jaeger-compact: {enabled: false}
jaeger-thrift: {enabled: false}
jaeger-grpc: {enabled: false}
zipkin: {enabled: false}
metrics: {enabled: false}
service:
enabled: false
# Журнал аудита — 0600 root:root. Root нужен как владелец файла, capabilities не нужны.
podSecurityContext:
runAsUser: 0
runAsGroup: 0
seccompProfile: {type: RuntimeDefault}
securityContext:
runAsUser: 0
runAsNonRoot: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
privileged: false
capabilities:
drop: [ALL]
resources:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
extraVolumes:
- name: audit-log
hostPath: {path: /var/log/audit, type: DirectoryOrCreate}
- name: otelcol-storage
hostPath: {path: /var/lib/otelcol-audit, type: DirectoryOrCreate}
# Креды Kafka — статические Secret'ы namespace (роли security-audit в Vault нет).
# Projected volume складывает оба Secret'а в /vault/secrets под теми же именами файлов,
# что читает конфиг: ${file:/vault/secrets/kafka-username|kafka-password}, ca_file=kafka-ca.crt.
- name: kafka-secrets
projected:
sources:
- secret:
name: security-audit-kafka
items:
- {key: username, path: kafka-username}
- {key: password, path: kafka-password}
- secret:
name: kafka-ca
items:
- {key: ca.crt, path: kafka-ca.crt}
extraVolumeMounts:
- {name: audit-log, mountPath: /var/log/audit, readOnly: true}
- {name: otelcol-storage, mountPath: /var/lib/otelcol-audit}
- {name: kafka-secrets, mountPath: /vault/secrets, readOnly: true}
# Метка исключает под из webhook'а инъекции Istio ещё до мутации (namespace без istio-injection).
podLabels:
sidecar.istio.io/inject: "false"
podAnnotations:
sidecar.istio.io/inject: "false"
# Контур-специфичное (CONTOUR, KAFKA_BROKERS) задаётся в оверлее через extraEnvs.
# Полный конфиг вместо мержа с дефолтами чарта.
alternateConfig:
extensions:
health_check:
endpoint: ${env:MY_POD_IP}:13133
file_storage/audit:
directory: /var/lib/otelcol-audit
compaction:
on_start: true
directory: /var/lib/otelcol-audit
receivers:
filelog/audit:
include:
- /var/log/audit/kube-apiserver-audit*.log # текущий и ротированные (lumberjack)
start_at: beginning
storage: file_storage/audit # позиции чтения переживают рестарт
max_log_size: 8MiB # дефолт 1MiB режет RequestResponse
poll_interval: 1s
include_file_name: false
operators:
- type: json_parser
parse_to: body
processors:
memory_limiter:
check_interval: 1s
limit_mib: 400
spike_limit_mib: 100
transform/audit:
error_mode: ignore
log_statements:
- context: log
statements:
# --- объём и шум (guard'ы на nil: у части событий requestObject есть, а metadata/annotations — nil) ---
- delete_key(body["requestObject"]["metadata"], "managedFields") where body["requestObject"] != nil and body["requestObject"]["metadata"] != nil
- delete_key(body["responseObject"]["metadata"], "managedFields") where body["responseObject"] != nil and body["responseObject"]["metadata"] != nil
- delete_key(body["requestObject"]["metadata"]["annotations"], "kubectl.kubernetes.io/last-applied-configuration") where body["requestObject"] != nil and body["requestObject"]["metadata"] != nil and body["requestObject"]["metadata"]["annotations"] != nil
- delete_key(body["responseObject"]["metadata"]["annotations"], "kubectl.kubernetes.io/last-applied-configuration") where body["responseObject"] != nil and body["responseObject"]["metadata"] != nil and body["responseObject"]["metadata"]["annotations"] != nil
# --- маскирование (п.23): значения, а не сама команда exec ---
- replace_pattern(body["requestURI"], "(?i)(password|passwd|pwd|token|secret|api[_-]?key)(=|%3D)[^&]*", "$$1$$2***")
# --- обогащение ---
- set(body["sarex_contour"], "${env:CONTOUR}")
- set(body["sarex_dry_run"], IsMatch(body["requestURI"], "[?&]dryRun=All"))
# --- действие ---
- set(body["sarex_action"], "read") where body["verb"] == "get" or body["verb"] == "list"
- set(body["sarex_action"], "mutate") where body["verb"] == "create" or body["verb"] == "update" or body["verb"] == "patch" or body["verb"] == "delete" or body["verb"] == "deletecollection"
- set(body["sarex_action"], "status") where body["objectRef"]["subresource"] == "status"
- set(body["sarex_action"], "interactive") where IsMatch(body["objectRef"]["subresource"], "^(exec|attach|portforward)$")
# --- исход ---
- set(body["sarex_outcome"], "applied") where body["responseStatus"]["code"] >= 200 and body["responseStatus"]["code"] < 300 and body["sarex_dry_run"] == false
- set(body["sarex_outcome"], "dry_run") where body["responseStatus"]["code"] >= 200 and body["responseStatus"]["code"] < 300 and body["sarex_dry_run"] == true
- set(body["sarex_outcome"], "denied_authn") where body["responseStatus"]["code"] == 401
- set(body["sarex_outcome"], "denied_authz") where body["responseStatus"]["code"] == 403 and body["annotations"]["authorization.k8s.io/decision"] == "forbid"
- set(body["sarex_outcome"], "denied_admission") where body["responseStatus"]["code"] >= 400 and body["responseStatus"]["code"] < 500 and body["annotations"]["authorization.k8s.io/decision"] == "allow" and body["responseStatus"]["code"] != 404 and body["responseStatus"]["code"] != 409 and body["responseStatus"]["code"] != 422
- set(body["sarex_outcome"], "invalid") where body["responseStatus"]["code"] == 422
- set(body["sarex_outcome"], "conflict") where body["responseStatus"]["code"] == 409
- set(body["sarex_outcome"], "not_found") where body["responseStatus"]["code"] == 404
- set(body["sarex_outcome"], "error") where body["responseStatus"]["code"] >= 500
# --- субъект (порядок важен: сначала общее, потом частное) ---
- set(body["sarex_actor_type"], "human")
- set(body["sarex_actor_type"], "serviceaccount") where IsMatch(body["user"]["username"], "^system:serviceaccount:")
- set(body["sarex_actor_type"], "controller") where IsMatch(body["user"]["username"], "^system:serviceaccount:kube-system:") or IsMatch(body["user"]["username"], "^system:(kube-|apiserver)")
- set(body["sarex_actor_type"], "node") where IsMatch(body["user"]["username"], "^system:node:")
- set(body["sarex_actor_type"], "gitops") where IsMatch(body["user"]["username"], "^system:serviceaccount:flux-system:")
batch:
send_batch_size: 500
send_batch_max_size: 1000
timeout: 2s
exporters:
kafka/audit:
brokers:
- ${env:KAFKA_BROKERS}
protocol_version: 2.8.0
topic: security.k8s-audit
encoding: raw # в сообщение — только body (JSON события), без OTLP-обёртки
auth:
sasl:
mechanism: SCRAM-SHA-512
username: ${file:/vault/secrets/kafka-username}
password: ${file:/vault/secrets/kafka-password}
tls:
insecure: false
ca_file: /vault/secrets/kafka-ca.crt
producer:
required_acks: -1
compression: zstd
max_message_bytes: 5242880
sending_queue:
enabled: true
storage: file_storage/audit # очередь на диске: Kafka недоступна → данные не теряются
queue_size: 10000
num_consumers: 2
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 60s
max_elapsed_time: 0 # повторять бесконечно; при полной очереди — backpressure на filelog
service:
extensions: [health_check, file_storage/audit]
pipelines:
logs/audit:
receivers: [filelog/audit]
processors: [memory_limiter, transform/audit, batch]
exporters: [kafka/audit]
telemetry:
logs:
level: info
metrics:
address: ${env:MY_POD_IP}:8888