From 2aaa45bf3c37e3c78b2eb89b9f8f72f736e35d88 Mon Sep 17 00:00:00 2001 From: emelinda Date: Mon, 2 Nov 2026 02:24:00 +0300 Subject: [PATCH] Add Vault audit logging integration for `uralkal` namespace with sidecars for file rotation, OTLP export, and Kafka integration --- .../authorizationpolicy-access-collector.yaml | 7 ++ .../base/helmrelease-access-collector.yaml | 26 +++++++ .../base/networkpolicy-access-collector.yaml | 10 +++ .../uralkal-audit/configmap-audit-rotate.yaml | 56 +++++++++++++++ .../configmap-audit-shipper.yaml | 68 ++++++++++++++++++ .../vault/uralkal-audit/kustomization.yaml | 20 ++++++ .../uralkal-audit/vault-audit-patch.yaml | 70 +++++++++++++++++++ 7 files changed, 257 insertions(+) create mode 100644 infrastructure/vault/uralkal-audit/configmap-audit-rotate.yaml create mode 100644 infrastructure/vault/uralkal-audit/configmap-audit-shipper.yaml create mode 100644 infrastructure/vault/uralkal-audit/kustomization.yaml create mode 100644 infrastructure/vault/uralkal-audit/vault-audit-patch.yaml diff --git a/infrastructure/security-audit/base/authorizationpolicy-access-collector.yaml b/infrastructure/security-audit/base/authorizationpolicy-access-collector.yaml index 77402b8..f2a140a 100644 --- a/infrastructure/security-audit/base/authorizationpolicy-access-collector.yaml +++ b/infrastructure/security-audit/base/authorizationpolicy-access-collector.yaml @@ -25,3 +25,10 @@ spec: namespaces: ["istio-system"] to: - operation: {ports: ["4317"]} + # Журнал аудита Vault: поды StatefulSet vault-vault-contour в mesh (ns vault, istio-injection=enabled). + # Если Vault выведут из mesh — principal пропадёт, будет 403, shipper копит очередь на PVC (см. README). + - from: + - source: + principals: ["cluster.local/ns/vault/sa/vault-vault-contour"] + to: + - operation: {ports: ["4319"]} diff --git a/infrastructure/security-audit/base/helmrelease-access-collector.yaml b/infrastructure/security-audit/base/helmrelease-access-collector.yaml index 80ae0fb..b4a5642 100644 --- a/infrastructure/security-audit/base/helmrelease-access-collector.yaml +++ b/infrastructure/security-audit/base/helmrelease-access-collector.yaml @@ -77,6 +77,13 @@ spec: servicePort: 8088 protocol: TCP appProtocol: http + # Журнал аудита Vault от sidecar'а audit-shipper в поде Vault (OTLP gRPC). + otlp-vault: + enabled: true + containerPort: 4319 + servicePort: 4319 + protocol: TCP + appProtocol: grpc otlp-http: {enabled: false} jaeger-compact: {enabled: false} jaeger-thrift: {enabled: false} @@ -149,6 +156,11 @@ spec: max_recv_msg_size_mib: 16 # Подпись HMAC (X-ZITADEL-Signature) не проверяется: доступ ограничен mTLS + AuthorizationPolicy # (только SA zitadel) + NetworkPolicy. Отвечает 200 после ConsumeLogs, т. е. после приёма в batch. + # Отдельный порт: доступ на 4319 только для SA Vault (AuthorizationPolicy), 4317 — только шлюз. + otlp/vault: + protocols: + grpc: + endpoint: "${env:MY_POD_IP}:4319" webhookevent/zitadel: endpoint: "${env:MY_POD_IP}:8088" path: /zitadel/events @@ -228,6 +240,16 @@ spec: producer: *kafka_producer sending_queue: *kafka_sending_queue # своя очередь у каждого экспортёра, общие только настройки retry_on_failure: *kafka_retry + # Body уже JSON-объект (json_parser в audit-shipper, sarex_* там же) — transform не нужен. + kafka/vault: + brokers: *kafka_brokers + protocol_version: *kafka_protocol_version + topic: security.vault-audit + encoding: raw + auth: *kafka_auth + producer: *kafka_producer + sending_queue: *kafka_sending_queue + retry_on_failure: *kafka_retry service: extensions: [health_check] pipelines: @@ -239,6 +261,10 @@ spec: receivers: [webhookevent/zitadel] processors: [memory_limiter, transform/idp, batch] exporters: [kafka/idp] + logs/vault: + receivers: [otlp/vault] + processors: [memory_limiter, batch] + exporters: [kafka/vault] telemetry: logs: {level: info} metrics: {address: "${env:MY_POD_IP}:8888"} diff --git a/infrastructure/security-audit/base/networkpolicy-access-collector.yaml b/infrastructure/security-audit/base/networkpolicy-access-collector.yaml index ae1eefc..3248bac 100644 --- a/infrastructure/security-audit/base/networkpolicy-access-collector.yaml +++ b/infrastructure/security-audit/base/networkpolicy-access-collector.yaml @@ -33,6 +33,16 @@ spec: app.kubernetes.io/component: start ports: - {port: 8088, protocol: TCP} + # Журнал аудита Vault (audit-shipper в подах StatefulSet). Метки — чарт vault-contour. + - from: + - namespaceSelector: + matchLabels: {kubernetes.io/metadata.name: vault} + podSelector: + matchLabels: + app.kubernetes.io/name: vault-contour + component: server + ports: + - {port: 4319, protocol: TCP} egress: # xDS и сертификаты istio-proxy от istiod. - to: diff --git a/infrastructure/vault/uralkal-audit/configmap-audit-rotate.yaml b/infrastructure/vault/uralkal-audit/configmap-audit-rotate.yaml new file mode 100644 index 0000000..f1602e7 --- /dev/null +++ b/infrastructure/vault/uralkal-audit/configmap-audit-rotate.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: audit-rotate-script + namespace: vault +data: + rotate.sh: | + #!/bin/sh + # Ротация журнала аудита Vault по размеру. POSIX sh (busybox), без set -x и без set -e: + # ошибка одной итерации не должна валить sidecar. В лог — только размер и число файлов, ничего из журнала. + # + # filelog в audit-shipper читает audit.log* и отслеживает файлы по fingerprint (первые байты), + # поэтому переименованный файл дочитывается. KEEP должен перекрывать максимальное отставание + # shipper'а: следить по метрикам otelcol и алерту на отставание (README). + # Каталог .otelcol (чекпоинты и очередь) не трогается. + set -u + LOG="${AUDIT_LOG:-/vault/audit/audit.log}" + MAX_BYTES="${MAX_BYTES:-104857600}" + KEEP="${KEEP:-5}" + INTERVAL="${INTERVAL:-30}" + + rotate() { + # pid проверяется ДО переименования: без SIGHUP Vault продолжит писать в audit.log.1, новый audit.log + # не появится, ротация остановится, а файл будет расти до заполнения диска (Vault fail-closed). + pid=$(pidof vault) + case "$pid" in + '' | *' '*) + echo "rotate: ОШИБКА: pid vault не найден или их несколько ('$pid'), ротация пропущена" + return 1 + ;; + esac + i=$KEEP + while [ "$i" -gt 1 ]; do + prev=$((i - 1)) + if [ -e "$LOG.$prev" ]; then mv -f "$LOG.$prev" "$LOG.$i"; fi + i=$prev + done + mv -f "$LOG" "$LOG.1" || return 1 + # Vault переоткрывает файл по SIGHUP; до этого события пишутся в audit.log.1 и не теряются. + kill -HUP "$pid" || echo "rotate: ОШИБКА: SIGHUP pid $pid не доставлен" + for f in "$LOG".*; do + n=${f##*.} + case "$n" in '' | *[!0-9]*) continue ;; esac + if [ "$n" -gt "$KEEP" ]; then rm -f "$f"; fi + done + echo "rotate: size=$1 files=$(ls -1 "$LOG"* 2>/dev/null | wc -l)" + } + + trap 'exit 0' TERM INT + while :; do + size=$(stat -c %s "$LOG" 2>/dev/null || echo 0) + if [ "$size" -gt "$MAX_BYTES" ]; then rotate "$size"; fi + # sleep в фоне + wait: SIGTERM при остановке пода обрабатывается сразу, а не после sleep. + sleep "$INTERVAL" & + wait $! + done diff --git a/infrastructure/vault/uralkal-audit/configmap-audit-shipper.yaml b/infrastructure/vault/uralkal-audit/configmap-audit-shipper.yaml new file mode 100644 index 0000000..838b85f --- /dev/null +++ b/infrastructure/vault/uralkal-audit/configmap-audit-shipper.yaml @@ -0,0 +1,68 @@ +# Конфиг sidecar'а audit-shipper. Без configMapGenerator намеренно: смена конфига не должна сама +# перезапускать Vault (рестарт = unseal). Изменения применяются ручным рестартом пода по процедуре README. +apiVersion: v1 +kind: ConfigMap +metadata: + name: audit-shipper-config + namespace: vault +data: + config.yaml: | + extensions: + file_storage/vault: + directory: /vault/audit/.otelcol + create_directory: true + compaction: + on_start: true + on_rebound: true # очередь bbolt иначе не уменьшается на диске + directory: /vault/audit/.otelcol + receivers: + filelog/vault: + include: [/vault/audit/audit.log*] + exclude: [/vault/audit/.otelcol/*] + start_at: beginning + storage: file_storage/vault + max_log_size: 4MiB + operators: + - type: json_parser + parse_to: body + processors: + memory_limiter: {check_interval: 1s, limit_mib: 150, spike_limit_mib: 50} + transform/vault: + error_mode: ignore + log_statements: + - context: log + statements: + # response-запись содержит и запрос — request-записи отбрасываем (объём ~вдвое меньше) + - set(body["sarex_drop"], true) where body["type"] == "request" + - set(body["sarex_kind"], "vault_audit") + - set(body["sarex_contour"], "${env:CONTOUR}") + - set(body["sarex_outcome"], "applied") + - set(body["sarex_outcome"], "denied_authz") where IsMatch(body["error"], "permission denied") + - set(body["sarex_outcome"], "denied_authn") where body["error"] != nil and body["error"] != "" and IsMatch(body["request"]["path"], "^auth/.+/login") + - set(body["sarex_actor_type"], "serviceaccount") where body["auth"]["metadata"]["service_account_name"] != nil + - set(body["sarex_actor_type"], "human") where body["sarex_actor_type"] == nil + filter/vault: + logs: + log_record: ['body["sarex_drop"] == true'] + batch: {send_batch_size: 500, timeout: 2s} + exporters: + otlp/audit: + endpoint: access-collector.security-audit.svc.cluster.local:4319 + # Vault и access-collector в mesh: plaintext до istio-proxy пода, дальше auto-mTLS. + # TODO(verify): у подов Vault есть istio-proxy (ns vault: istio-injection=enabled). + tls: {insecure: true} + # Очередь на диске ограничена: 20000 батчей по 500 записей не помещаются в PVC, а полный диск + # останавливает Vault. При заполненной очереди filelog перестаёт читать — буфером служат сами файлы + # журнала (KEEP × MAX_BYTES). TODO(verify): queue_size по замеру объёма. + sending_queue: {enabled: true, storage: file_storage/vault, queue_size: 1000} + retry_on_failure: {enabled: true, max_elapsed_time: 0} + service: + extensions: [file_storage/vault] + pipelines: + logs: + receivers: [filelog/vault] + processors: [memory_limiter, transform/vault, filter/vault, batch] + exporters: [otlp/audit] + telemetry: + logs: {level: info} + metrics: {address: "${env:MY_POD_IP}:8888"} diff --git a/infrastructure/vault/uralkal-audit/kustomization.yaml b/infrastructure/vault/uralkal-audit/kustomization.yaml new file mode 100644 index 0000000..0d28a7d --- /dev/null +++ b/infrastructure/vault/uralkal-audit/kustomization.yaml @@ -0,0 +1,20 @@ +# Vault Уралкалия + журнал аудита (file device → sidecar'ы → access-collector). НЕ ПОДКЛЮЧЁН. +# Включение — замена ../../infrastructure/vault/uralkal на ../../infrastructure/vault/uralkal-audit +# в clusters/uralkal/kustomization.yaml по процедуре из infrastructure/security-audit/README.md +# («Журнал аудита Vault»): VCT меняется → StatefulSet пересоздаётся с --cascade=orphan, поды — unseal. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../uralkal + # PVC отдельным файлом не нужен: 3 реплики (HA raft) → том через server.auditStorage (volumeClaimTemplates). + - configmap-audit-shipper.yaml + - configmap-audit-rotate.yaml +patches: + # JSON6902 только в spec.values: всё нужное чарт vault-contour 0.2.3 умеет через values. + # spec.postRenderers (nodeSelector/tolerations из ../uralkal) не трогается. + - path: vault-audit-patch.yaml + target: + group: helm.toolkit.fluxcd.io + kind: HelmRelease + name: vault + namespace: vault diff --git a/infrastructure/vault/uralkal-audit/vault-audit-patch.yaml b/infrastructure/vault/uralkal-audit/vault-audit-patch.yaml new file mode 100644 index 0000000..cb84f3d --- /dev/null +++ b/infrastructure/vault/uralkal-audit/vault-audit-patch.yaml @@ -0,0 +1,70 @@ +# Журнал аудита Vault: том /vault/audit, общий PID namespace и два sidecar'а. +# Чарт vault-contour 0.2.3: auditStorage → volumeClaimTemplates "audit" + mount в контейнер vault; +# extraContainers/volumes/shareProcessNamespace — values (toYaml, без tpl). +# +# Vault — fail-closed: включённый audit device, который не может записать событие, останавливает обслуживание +# запросов. Поэтому сеть в пути записи нет (файл + sidecar), а объём на диске ограничен ротацией и очередью. +- op: add + path: /spec/values/server/auditStorage + value: + enabled: true + size: 5Gi # TODO(verify): по замеру объёма журнала (KEEP × MAX_BYTES + очередь + запас) + storageClass: local-path + accessMode: ReadWriteOnce + mountPath: /vault/audit +# Нужен audit-rotate для SIGHUP процессу vault. Побочный эффект: sidecar'ы с UID vault видят его процесс +# (/proc//environ; память — при разрешающем kernel.yama.ptrace_scope). См. README, «Решения с последствиями». +- op: add + path: /spec/values/server/shareProcessNamespace + value: true +- op: add + path: /spec/values/server/volumes + value: + - name: audit-shipper-config + configMap: {name: audit-shipper-config} + - name: audit-rotate-script + configMap: {name: audit-rotate-script} +- op: add + path: /spec/values/server/extraContainers + value: + # filelog → OTLP в access-collector. Чекпоинты и очередь отправки — на том же PVC. + # Кредов Kafka здесь нет: Vault Agent в поде Vault невозможен (Vault стартует запечатанным). + - name: audit-shipper + image: cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64 + args: ["--config=/conf/config.yaml"] + env: + - {name: CONTOUR, value: uralkal} + - name: MY_POD_IP + valueFrom: {fieldRef: {fieldPath: status.podIP}} + - {name: GOMEMLIMIT, value: 200MiB} + securityContext: &audit-sidecar-security + # UID/GID процесса vault (podSecurityContext чарта: 100/1000): файл журнала 0600, владелец — vault. + runAsUser: 100 + runAsGroup: 1000 + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: {drop: [ALL]} + seccompProfile: {type: RuntimeDefault} + resources: + requests: {cpu: 25m, memory: 64Mi} + limits: {cpu: 200m, memory: 256Mi} + volumeMounts: + - {name: audit, mountPath: /vault/audit} + - {name: audit-shipper-config, mountPath: /conf, readOnly: true} + # Ротация по размеру + SIGHUP vault (file device по SIGHUP переоткрывает файл). + - name: audit-rotate + # busybox 1.37.0 из registry контура (helperImage local-path-provisioner); тег latest закреплён digest'ом. + image: cr.yandex/crp3ccidau046kdj8g9q/contour/local-path-provisioner-nn/busybox@sha256:11f85134f388cff5f4c66f9bb4c5942249c1f6f7eb8b3889948d953487b5f7a8 + command: ["/bin/sh", "/scripts/rotate.sh"] + env: + - {name: MAX_BYTES, value: "104857600"} + - {name: KEEP, value: "5"} + - {name: INTERVAL, value: "30"} + securityContext: *audit-sidecar-security # тот же UID: иначе нельзя отправить сигнал vault + resources: + requests: {cpu: 5m, memory: 16Mi} + limits: {cpu: 50m, memory: 32Mi} + volumeMounts: + - {name: audit, mountPath: /vault/audit} + - {name: audit-rotate-script, mountPath: /scripts, readOnly: true}