Add Vault audit logging integration for uralkal namespace with sidecars for file rotation, OTLP export, and Kafka integration
This commit is contained in:
parent
2aaa45bf3c
commit
30f1d32fc5
@ -1,12 +1,13 @@
|
||||
# security-audit
|
||||
|
||||
Поток ИБ в Kafka, три источника:
|
||||
Поток ИБ в Kafka, четыре источника:
|
||||
|
||||
| Источник | Коллектор | Топик |
|
||||
|---|---|---|
|
||||
| журнал аудита kube-apiserver (мастера) | DaemonSet `audit-collector` | `security.k8s-audit` |
|
||||
| журнал доступа ingress-шлюза Istio | Deployment `access-collector`, pipeline `logs/access` | `security.mesh-access` |
|
||||
| события Zitadel (Actions v2) | Deployment `access-collector`, pipeline `logs/idp` | `security.idp-events` |
|
||||
| журнал аудита Vault | sidecar `audit-shipper` в подах Vault → `access-collector`, pipeline `logs/vault` | `security.vault-audit` |
|
||||
|
||||
Одно сообщение — одна запись в JSON (`encoding: raw`, без OTLP-обёртки), обогащённая полями `sarex_*`.
|
||||
Дальнейшие этапы (processor → MinIO/Parquet) — отдельными компонентами.
|
||||
@ -19,6 +20,7 @@
|
||||
| `base/idp-actions-bootstrap/` | Job настройки Target/Execution'ов в Zitadel, скрипт и список событий | `security-audit` |
|
||||
| `mesh/` | EnvoyFilter и Telemetry шлюза; без `namespace` в kustomization, селектор шлюза — заглушка | `istio-system` |
|
||||
| `uralkal/` | оверлей: `base` + `mesh`, селектор шлюза, Kafka, `dependsOn` | — |
|
||||
| `../vault/uralkal-audit/` | Vault-часть журнала аудита Vault: оверлей поверх `vault/uralkal` (не подключён) | `vault` |
|
||||
|
||||
| Оверлей | Статус |
|
||||
|---|---|
|
||||
@ -460,6 +462,167 @@ kubectl -n security-audit get job idp-actions-bootstrap -o json \
|
||||
Пока модуль подключён, Flux вернёт Execution'ы при следующем пересоздании Job'а. Для постоянного отключения убрать
|
||||
`idp-actions-bootstrap` из `base/kustomization.yaml` или модуль из кластера.
|
||||
|
||||
## Журнал аудита Vault
|
||||
|
||||
П.19 документа ИБ: аудит Vault включён, чувствительные значения хешируются HMAC-SHA256, журнал идёт в поток ИБ.
|
||||
П.23: секретов в журнале нет.
|
||||
|
||||
```
|
||||
Vault ──audit device "file"──► /vault/audit/audit.log (VCT "audit", свой PVC у каждой реплики)
|
||||
│ sidecar audit-rotate (busybox): ротация по размеру + SIGHUP vault (shareProcessNamespace)
|
||||
│ sidecar audit-shipper (otelcol-contrib): filelog, чекпоинты и очередь — /vault/audit/.otelcol
|
||||
▼ OTLP gRPC, mTLS (Vault и коллектор в mesh)
|
||||
access-collector: otlp/vault :4319 → logs/vault → Kafka security.vault-audit
|
||||
```
|
||||
|
||||
Vault-часть — оверлей `infrastructure/vault/uralkal-audit/` (наследует `vault/uralkal`), **не подключён**.
|
||||
|
||||
### Почему файл и sidecar
|
||||
|
||||
Vault работает **fail-closed**: если включённый audit device не может записать событие, Vault перестаёт обслуживать
|
||||
запросы. Поэтому в пути записи нет сети, только локальный файл. Kafka, коллектор и сеть могут лежать сколько угодно:
|
||||
Vault пишет в файл, shipper догоняет потом. Остаётся один риск того же класса — **полный диск**: ротация и очередь
|
||||
ограничены, на заполнение PVC нужен алерт.
|
||||
|
||||
Что хешируется: file device по умолчанию заменяет все строковые значения запросов и ответов (секреты, токены,
|
||||
`client_token`, accessor'ы) на `hmac-sha256:…` с ключом, известным только Vault. Одинаковые значения дают одинаковый
|
||||
хеш: по нему можно сверять события (`vault write sys/audit-hash/file input=…`), не раскрывая значения.
|
||||
**`log_raw` не включается никогда**: с ним в журнал попадают секреты открытым текстом, а журнал уходит в Kafka и S3.
|
||||
|
||||
Кредов Kafka у sidecar'а нет и быть не может: Vault Agent в поде самого Vault невозможен, Vault стартует запечатанным.
|
||||
Креды есть только у `access-collector`.
|
||||
|
||||
### Решения с последствиями
|
||||
|
||||
- **Том: VCT, а не отдельный PVC.** В Уралкалии 3 реплики HA/raft (дефолт чарта, `ha.enabled: true`, `replicas: 3`;
|
||||
`vault/uralkal` это не меняет). Каждой реплике нужен свой том. Это `server.auditStorage` чарта →
|
||||
`volumeClaimTemplates: audit` (5Gi `local-path`, `TODO(verify)` объём). `volumeClaimTemplates` — **неизменяемое поле**
|
||||
StatefulSet: обычный `helm upgrade` упадёт, нужна процедура с `kubectl delete sts --cascade=orphan` (ниже).
|
||||
- **Unseal вручную.** `vault-unseal` в Уралкалии не подключён (есть только в `vk-infra`, `yc-infra-prod`). Ключи — в Secret
|
||||
`vault/vault-secret` (`docs/closed-contour-deployment.md`). Новый pod template перезапустит все 3 пода, каждый
|
||||
стартует sealed. Readiness чарта — `vault status`: sealed-под не Ready, и `RollingUpdate` ждёт его unseal, прежде чем
|
||||
перейти к следующему. Поды распечатываются по одному, кворум raft (2 из 3) сохраняется.
|
||||
- **mTLS без PERMISSIVE.** У подов Vault есть istio-proxy: ns `vault` с `istio-injection: enabled`, у чарта нет opt-out.
|
||||
`PeerAuthentication` в контуре нет, т. е. PERMISSIVE по умолчанию. shipper шлёт plaintext gRPC (`tls.insecure: true`) до
|
||||
своего istio-proxy, дальше auto-mTLS. На коллекторе AuthorizationPolicy: 4319 только от
|
||||
`cluster.local/ns/vault/sa/vault-vault-contour`, плюс NetworkPolicy только из ns `vault`, `app.kubernetes.io/name=vault-contour`,
|
||||
`component=server`. `TODO(verify)`: `kubectl -n vault get pod vault-vault-contour-0 -o jsonpath='{.spec.containers[*].name}'`
|
||||
содержит `istio-proxy`. Если Vault окажется вне mesh, principal не будет, коллектор ответит 403, shipper накопит
|
||||
очередь, а потом перестанет читать файл. Vault продолжит работать, но после KEEP ротаций начнутся потери.
|
||||
Тогда — вариант (б): TLS на `otlp/vault` (cert-manager), CA в sidecar. Вариант (а) (PERMISSIVE на 4319) ничего не даёт,
|
||||
пока нет STRICT, а principal без mTLS всё равно пуст.
|
||||
- **Общий PID namespace и UID vault у sidecar'ов.** SIGHUP можно отправить только процессу своего UID: CAP_KILL у
|
||||
non-root контейнера не работает, ambient capabilities нет. Файл `0600` читается только UID 100. Последствие:
|
||||
sidecar'ы видят процесс vault и могут читать `/proc/<pid>/environ`, а при разрешающем `kernel.yama.ptrace_scope` и
|
||||
dumpable-процессе — и его память. Смягчение: образы только из registry контура (busybox закреплён digest'ом),
|
||||
`readOnlyRootFilesystem`, `drop: [ALL]`, `no-new-privileges`, без shell-доступа в под. `TODO(verify)`: значение
|
||||
`ptrace_scope` на узлах `dedicated: generic`.
|
||||
- **Отклонения от исходного шаблона:**
|
||||
- `rotate.sh` проверяет pid vault **до** `mv`. Без SIGHUP Vault продолжил бы писать в `audit.log.1`, новый файл
|
||||
не появился бы, ротация встала бы, а файл рос бы до заполнения диска;
|
||||
- `queue_size` shipper'а — 1000, а не 20000: 20000 батчей по 500 записей не помещаются в PVC. При полной очереди
|
||||
filelog перестаёт читать, и буфером служат сами файлы (KEEP × MAX_BYTES);
|
||||
- `compaction` у `file_storage`: иначе файл bbolt-очереди не уменьшается.
|
||||
- **ConfigMap'ы без хеша в имени** (`audit-shipper-config`, `audit-rotate-script`): правка не перезапускает Vault сама
|
||||
(рестарт = unseal). Применить — ручной рестарт подов по одному с unseal.
|
||||
|
||||
### Включение audit device
|
||||
|
||||
Через terraform контура (`infra/terraform`, ветка `contour`, рядом с `vault-platform-secrets`) — в этой задаче не сделано:
|
||||
|
||||
```hcl
|
||||
resource "vault_audit" "file" {
|
||||
type = "file"
|
||||
path = "file"
|
||||
options = {
|
||||
file_path = "/vault/audit/audit.log"
|
||||
mode = "0600"
|
||||
format = "json"
|
||||
elide_list_responses = "true" # Vault 1.21.2 (образ vault:1.21.2) — поддерживается
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Альтернатива — CLI на активном узле: `vault audit enable file file_path=/vault/audit/audit.log mode=0600 format=json elide_list_responses=true`.
|
||||
|
||||
Audit device общий для кластера raft: **все** реплики должны иметь `/vault/audit` до включения. Иначе узел без тома,
|
||||
став активным, не сможет писать журнал и перестанет обслуживать запросы.
|
||||
|
||||
### Порядок подключения (строго)
|
||||
|
||||
1. Топик `security.vault-audit`: 3 партиции, `retention.ms=259200000`, RF по контуру. Пользователь `security-audit`,
|
||||
ACL не нужны (как у остальных топиков модуля).
|
||||
2. `security-audit` подключён и работает: `access-collector` с `logs/vault`, порт 4319 в Service.
|
||||
3. Подготовка Vault:
|
||||
- `vault operator raft list-peers`: 3 voter'а, лидер есть;
|
||||
- `vault operator raft snapshot save /tmp/pre-audit.snap`, снапшот скопировать из пода;
|
||||
- ключи unseal из `vault/vault-secret` под рукой.
|
||||
4. Переключение оверлея. VCT неизменяем, поэтому HelmRelease приостанавливается до пересоздания StatefulSet:
|
||||
```bash
|
||||
flux suspend helmrelease vault -n vault
|
||||
```
|
||||
MR: в `clusters/uralkal/kustomization.yaml` `../../infrastructure/vault/uralkal` → `../../infrastructure/vault/uralkal-audit`,
|
||||
дождаться применения Kustomization `flux-system`: ConfigMap'ы появились, HelmRelease обновлён, но приостановлен.
|
||||
```bash
|
||||
kubectl -n vault get cm audit-shipper-config audit-rotate-script
|
||||
kubectl -n vault delete sts vault-vault-contour --cascade=orphan # поды и PVC data остаются, Vault работает
|
||||
kubectl -n vault get pods -l component=server # все 3 Running, unsealed
|
||||
flux resume helmrelease vault -n vault # helm создаёт STS с новым template и VCT audit
|
||||
```
|
||||
5. RollingUpdate пересоздаёт поды с конца (`-2`, `-1`, `-0`). Каждый новый под стартует sealed:
|
||||
`kubectl -n vault exec vault-vault-contour-N -c vault -- vault operator unseal` (порог ключей), дождаться Ready,
|
||||
и только потом следующий под.
|
||||
6. После раскатки:
|
||||
- 3 пода Ready, в каждом контейнеры `vault`, `audit-shipper`, `audit-rotate`, `istio-proxy`;
|
||||
- PVC `audit-vault-vault-contour-{0,1,2}` в состоянии Bound;
|
||||
- `vault operator raft list-peers` — 3 voter'а;
|
||||
- на каждом поде `kubectl -n vault exec vault-vault-contour-N -c vault -- sh -c 'touch /vault/audit/.w && rm /vault/audit/.w'`.
|
||||
7. Только теперь — `vault_audit` (terraform) или `vault audit enable`. Если включить раньше, enable упадёт: Vault
|
||||
проверяет запись при включении.
|
||||
8. Проверка (ниже).
|
||||
|
||||
### ⚠ Откат
|
||||
|
||||
**Сначала** `vault audit disable file` (или убрать `vault_audit` из terraform и применить), убедиться, что
|
||||
`vault audit list` пуст. **Потом** вернуть `vault/uralkal`. Удаление VCT — тоже изменение неизменяемого поля, поэтому
|
||||
та же процедура: suspend → `delete sts --cascade=orphan` → resume → unseal по одному. PVC `audit-*` после отката
|
||||
остаются, удалить вручную, когда журнал уже не нужен.
|
||||
|
||||
Если вернуть оверлей с включённым audit device, у Vault пропадёт `/vault/audit`, он не сможет писать журнал и
|
||||
перестанет обслуживать запросы.
|
||||
|
||||
### Проверка после подключения
|
||||
|
||||
- `vault audit list`: device `file/`.
|
||||
- Чтение тестового секрета → событие в `security.vault-audit` с `sarex_kind=vault_audit`, значение —
|
||||
`hmac-sha256:…`, записи `"type": "request"` нет.
|
||||
- Запрос без прав → `sarex_outcome=denied_authz`; неудачный логин → `denied_authn`.
|
||||
- `kubectl -n security-audit scale deploy/access-collector --replicas=0` на 5 минут → Vault отвечает. После возврата
|
||||
2 реплик события дочитаны без пропусков (сверка `request.id`).
|
||||
- Искусственная ротация: `MAX_BYTES` поменять в ConfigMap нельзя, это env в values. Временно на одном поде:
|
||||
`kubectl -n vault exec vault-vault-contour-N -c audit-rotate -- sh -c 'MAX_BYTES=1 INTERVAL=1 timeout 5 sh /scripts/rotate.sh'`
|
||||
→ появился `audit.log.1`, новый `audit.log` создан Vault, события не потеряны.
|
||||
|
||||
### Мониторинг и алерты
|
||||
|
||||
- **Заполнение PVC `audit-*` > 80% — критично:** при полном диске Vault встаёт. Метрика `kubelet_volume_stats_*`.
|
||||
- Ошибки и отставание `audit-shipper`:
|
||||
- `otelcol_exporter_send_failed_log_records{exporter="otlp/audit"}`;
|
||||
- `otelcol_exporter_queue_size` против `otelcol_exporter_queue_capacity`.
|
||||
|
||||
Метрики на `:8888` пода Vault; сбора метрик в Уралкалии пока нет.
|
||||
- Отключение audit device: событие с путём `sys/audit/*` в самом журнале → правило алерта в processor.
|
||||
- Строки `rotate: ОШИБКА` в логе `audit-rotate`.
|
||||
|
||||
### Открытые вопросы
|
||||
|
||||
- Размер PVC, `MAX_BYTES`, `KEEP`, `queue_size` — по замеру объёма. `KEEP × MAX_BYTES` должен перекрывать худший простой
|
||||
коллектора, а их сумма с очередью — оставаться ниже 80% PVC.
|
||||
- Фильтрация шумных путей средствами Vault: в OSS есть только `elide_list_responses` и `hmac_accessor`; фильтры audit
|
||||
(`filter`) — Enterprise. Поэтому `request`-записи отбрасываются в shipper'е.
|
||||
- Какой узел что пишет: standby в OSS пересылает запросы на активный, поэтому основной поток — с лидера; shipper есть на
|
||||
всех трёх.
|
||||
|
||||
## Проверить перед подключением
|
||||
|
||||
Сверено по репозиторию и чартам 01.11.2026, доступа к кластеру не было.
|
||||
@ -493,6 +656,17 @@ kubectl -n security-audit get job idp-actions-bootstrap -o json \
|
||||
| `webhookevent` 0.121 | есть в образе; 200 после `ConsumeLogs` (с `batch` — после приёма в батч), 500 при ошибке; одна запись на запрос; `required_header` есть, `header_attribute_regex` нет | исходники receiver v0.121.0; локальный прогон |
|
||||
| OTTL | `ParseJSON`, `Decode(…, "base64")`, `IsString`, `IsMatch`, `IsMap`, литерал map, `set` во вложенный отсутствующий map — работают | `validate` и прогон 6 событий: канарейки (хеш, base64, не-JSON) не дошли |
|
||||
| Образ Job | `curl` + `jq` вместе нет; `kubectl:1.33.4` (jq 1.6) + `kafka:4.0.0-debian-12-r10` (curl 7.88) | `docker run` образов из registry контура |
|
||||
| **Журнал аудита Vault** (02.11.2026) | | |
|
||||
| Чарт `vault-contour` 0.2.3 | values: `server.auditStorage` (VCT `audit` + mount в vault), `extraContainers`, `volumes`, `volumeMounts`, `shareProcessNamespace`, `extraEnvironmentVars` — postRenderers не нужны | шаблоны `server-statefulset.yaml`, `_helpers.tpl` |
|
||||
| Режим Vault | HA raft, 3 реплики, VCT `data`, `RollingUpdate`, `podManagementPolicy: Parallel`; UID/GID 100/1000, `fsGroup: 1000`; SA `vault-vault-contour`; метки `app.kubernetes.io/name=vault-contour`, `component=server` | `helm template` с values `vault/uralkal` |
|
||||
| Unseal | ручной: `vault-unseal` в Уралкалии не подключён; readiness `vault status` держит rollout до unseal | `clusters/*`, `docs/closed-contour-deployment.md`, `values.yaml` чарта |
|
||||
| Istio у Vault | ns `vault` `istio-injection: enabled`, opt-out в чарте нет → в mesh; PeerAuthentication нет | рендер, `infrastructure/vault/base/namespace.yaml` |
|
||||
| Версия Vault | 1.21.2: `elide_list_responses` есть, file device по SIGHUP переоткрывает файл | тег образа |
|
||||
| Образы | `opentelemetry-collector-contrib:0.121.0-amd64`; busybox 1.37.0 `contour/local-path-provisioner-nn/busybox@sha256:11f8…` (sh, stat, pidof, kill, mv) | `docker run` из registry |
|
||||
| `vault/uralkal-audit` | собирается; postRenderers (nodeSelector/tolerations) на месте; ConfigMap'ы в `vault`; `vault/uralkal` побайтно тот же | `kubectl kustomize`, diff |
|
||||
| Итоговый StatefulSet | `shareProcessNamespace: true`, VCT `data` + `audit`, оба sidecar'а UID 100/GID 1000, `/vault/audit` rw у vault и sidecar'ов | `helm template` + post-render тем же kustomize-патчем, что у Flux |
|
||||
| Ротация и доставка | фиктивный `vault` (по SIGHUP переоткрывает файл) + `rotate.sh` (`MAX_BYTES=20000`, 16 ротаций) + shipper + access-collector: 1500/1500 событий, 0 дублей, `request` отброшены, `denied_authz` размечен; то же при остановке коллектора на 25 с | Docker: общий PID namespace, том с правами 100:1000/0600, `--read-only`, `--cap-drop ALL` |
|
||||
| `rotate.sh` | `sh -n` busybox/dash/bash --posix — OK; без процесса vault ротация пропускается (файл не переименован); SIGTERM — выход за 0 с | busybox из registry. shellcheck на машине нет |
|
||||
| Скрипт | `sh -n` dash/bash --posix/busybox — OK; mock: создание, повтор без изменений, объединение с чужим target, обновление endpoint, `--remove`, API недоступен, 401; утечек PAT/signingKey нет | mock Zitadel на Python, запуск в образе kafka с `--read-only`, uid 1001. shellcheck на машине нет |
|
||||
|
||||
Остаётся открытым:
|
||||
@ -509,5 +683,8 @@ kubectl -n security-audit get job idp-actions-bootstrap -o json \
|
||||
исходникам должно работать, запрос не выполнялся.
|
||||
- `TODO(verify)` **allowlist полей** `details` по реальным событиям; **лимит ~64 КиБ** тела у `webhookevent`.
|
||||
- `TODO(verify)` **toolbox-образ** для Job вместо пары kubectl + kafka.
|
||||
- `TODO(verify)` **Vault:** istio-proxy у подов Vault; `ptrace_scope` на узлах; объём PVC `audit`, `MAX_BYTES`/`KEEP`/`queue_size`
|
||||
по замеру; создание STS заново через `helm upgrade` после `--cascade=orphan` (Helm создаёт отсутствующий ресурс — проверить
|
||||
на стенде до Уралкалия).
|
||||
- Некритичные предупреждения 0.121.0: `service::telemetry::metrics::address` устарел (так же во всех коллекторах репо);
|
||||
OTTL-пути без префикса `log.` (контекст выводится автоматически).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user