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