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 # = роль Vault security-audit, bound SA 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} extraVolumeMounts: - {name: audit-log, mountPath: /var/log/audit, readOnly: true} - {name: otelcol-storage, mountPath: /var/lib/otelcol-audit} # Метка исключает под из webhook'а инъекции Istio ещё до мутации (namespace без istio-injection). podLabels: sidecar.istio.io/inject: "false" podAnnotations: sidecar.istio.io/inject: "false" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-init-first: "true" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/agent-requests-cpu: "25m" vault.hashicorp.com/agent-limits-cpu: "100m" vault.hashicorp.com/auth-path: auth/kubernetes vault.hashicorp.com/role: security-audit # Чарт рендерит podAnnotations через toYaml | tpl: однострочный шаблон toYaml переносит, # и литерал {{ "..." }} рвётся. Поэтому по строке на действие (block scalar не переносится). vault.hashicorp.com/agent-inject-secret-kafka-username: secrets/data/security-audit/kafka vault.hashicorp.com/agent-inject-template-kafka-username: |- {{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }} {{ "{{ .Data.data.username }}" }} {{ "{{- end -}}" }} vault.hashicorp.com/agent-inject-secret-kafka-password: secrets/data/security-audit/kafka vault.hashicorp.com/agent-inject-template-kafka-password: |- {{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }} {{ "{{ .Data.data.password }}" }} {{ "{{- end -}}" }} vault.hashicorp.com/agent-inject-secret-kafka-ca.crt: secrets/data/security-audit/kafka vault.hashicorp.com/agent-inject-template-kafka-ca.crt: |- {{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }} {{ "{{ .Data.data.ca_crt }}" }} {{ "{{- end -}}" }} # Контур-специфичное (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: # --- объём и шум --- - delete_key(body["requestObject"]["metadata"], "managedFields") where body["requestObject"] != nil - delete_key(body["responseObject"]["metadata"], "managedFields") where body["responseObject"] != nil - delete_key(body["requestObject"]["metadata"]["annotations"], "kubectl.kubernetes.io/last-applied-configuration") where body["requestObject"] != nil - delete_key(body["responseObject"]["metadata"]["annotations"], "kubectl.kubernetes.io/last-applied-configuration") where body["responseObject"] != 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