Add Vault audit logging integration for uralkal namespace with sidecars for file rotation, OTLP export, and Kafka integration
This commit is contained in:
parent
2f5e37f598
commit
2aaa45bf3c
@ -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"]}
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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"}
|
||||
20
infrastructure/vault/uralkal-audit/kustomization.yaml
Normal file
20
infrastructure/vault/uralkal-audit/kustomization.yaml
Normal 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
|
||||
70
infrastructure/vault/uralkal-audit/vault-audit-patch.yaml
Normal file
70
infrastructure/vault/uralkal-audit/vault-audit-patch.yaml
Normal 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}
|
||||
Loading…
Reference in New Issue
Block a user