Add Vault audit logging integration for uralkal namespace with sidecars for file rotation, OTLP export, and Kafka integration

This commit is contained in:
emelinda 2026-11-02 02:24:00 +03:00
parent 2f5e37f598
commit 2aaa45bf3c
7 changed files with 257 additions and 0 deletions

View File

@ -25,3 +25,10 @@ spec:
namespaces: ["istio-system"] namespaces: ["istio-system"]
to: to:
- operation: {ports: ["4317"]} - 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"]}

View File

@ -77,6 +77,13 @@ spec:
servicePort: 8088 servicePort: 8088
protocol: TCP protocol: TCP
appProtocol: http 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} otlp-http: {enabled: false}
jaeger-compact: {enabled: false} jaeger-compact: {enabled: false}
jaeger-thrift: {enabled: false} jaeger-thrift: {enabled: false}
@ -149,6 +156,11 @@ spec:
max_recv_msg_size_mib: 16 max_recv_msg_size_mib: 16
# Подпись HMAC (X-ZITADEL-Signature) не проверяется: доступ ограничен mTLS + AuthorizationPolicy # Подпись HMAC (X-ZITADEL-Signature) не проверяется: доступ ограничен mTLS + AuthorizationPolicy
# (только SA zitadel) + NetworkPolicy. Отвечает 200 после ConsumeLogs, т. е. после приёма в batch. # (только SA zitadel) + NetworkPolicy. Отвечает 200 после ConsumeLogs, т. е. после приёма в batch.
# Отдельный порт: доступ на 4319 только для SA Vault (AuthorizationPolicy), 4317 — только шлюз.
otlp/vault:
protocols:
grpc:
endpoint: "${env:MY_POD_IP}:4319"
webhookevent/zitadel: webhookevent/zitadel:
endpoint: "${env:MY_POD_IP}:8088" endpoint: "${env:MY_POD_IP}:8088"
path: /zitadel/events path: /zitadel/events
@ -228,6 +240,16 @@ spec:
producer: *kafka_producer producer: *kafka_producer
sending_queue: *kafka_sending_queue # своя очередь у каждого экспортёра, общие только настройки sending_queue: *kafka_sending_queue # своя очередь у каждого экспортёра, общие только настройки
retry_on_failure: *kafka_retry 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: service:
extensions: [health_check] extensions: [health_check]
pipelines: pipelines:
@ -239,6 +261,10 @@ spec:
receivers: [webhookevent/zitadel] receivers: [webhookevent/zitadel]
processors: [memory_limiter, transform/idp, batch] processors: [memory_limiter, transform/idp, batch]
exporters: [kafka/idp] exporters: [kafka/idp]
logs/vault:
receivers: [otlp/vault]
processors: [memory_limiter, batch]
exporters: [kafka/vault]
telemetry: telemetry:
logs: {level: info} logs: {level: info}
metrics: {address: "${env:MY_POD_IP}:8888"} metrics: {address: "${env:MY_POD_IP}:8888"}

View File

@ -33,6 +33,16 @@ spec:
app.kubernetes.io/component: start app.kubernetes.io/component: start
ports: ports:
- {port: 8088, protocol: TCP} - {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: egress:
# xDS и сертификаты istio-proxy от istiod. # xDS и сертификаты istio-proxy от istiod.
- to: - to:

View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -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/<pid>/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}