В контуре uralkal роли Vault (security-audit, security-audit-bootstrap) не заведены.
Коллекторы и Job idp-actions-bootstrap монтируют Secret'ы namespace (security-audit-kafka,
kafka-ca) projected volume'ом в /vault/secrets под теми же именами файлов — конфиг
коллекторов (${file:...}, ca_file) не меняется. Из NetworkPolicy убран egress в Vault,
из оверлея uralkal — dependsOn на HelmRelease vault.
238 lines
11 KiB
YAML
238 lines
11 KiB
YAML
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:
|
||
# --- объём и шум ---
|
||
- 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
|