iac/infrastructure/security-audit/base/helmrelease-collector.yaml

246 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 # = роль 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