**Add Zitadel events ingestion pipeline with AuthorizationPolicy and initial Job configuration for security-audit namespace**
This commit is contained in:
parent
ae1a683ada
commit
2f5e37f598
@ -1,11 +1,12 @@
|
|||||||
# security-audit
|
# security-audit
|
||||||
|
|
||||||
Поток ИБ в Kafka, два источника:
|
Поток ИБ в Kafka, три источника:
|
||||||
|
|
||||||
| Источник | Коллектор | Топик |
|
| Источник | Коллектор | Топик |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| журнал аудита kube-apiserver (мастера) | DaemonSet `audit-collector` | `security.k8s-audit` |
|
| журнал аудита kube-apiserver (мастера) | DaemonSet `audit-collector` | `security.k8s-audit` |
|
||||||
| журнал доступа ingress-шлюза Istio | Deployment `access-collector` | `security.mesh-access` |
|
| журнал доступа ingress-шлюза Istio | Deployment `access-collector`, pipeline `logs/access` | `security.mesh-access` |
|
||||||
|
| события Zitadel (Actions v2) | Deployment `access-collector`, pipeline `logs/idp` | `security.idp-events` |
|
||||||
|
|
||||||
Одно сообщение — одна запись в JSON (`encoding: raw`, без OTLP-обёртки), обогащённая полями `sarex_*`.
|
Одно сообщение — одна запись в JSON (`encoding: raw`, без OTLP-обёртки), обогащённая полями `sarex_*`.
|
||||||
Дальнейшие этапы (processor → MinIO/Parquet) — отдельными компонентами.
|
Дальнейшие этапы (processor → MinIO/Parquet) — отдельными компонентами.
|
||||||
@ -14,7 +15,8 @@
|
|||||||
|
|
||||||
| Каталог | Что | Namespace |
|
| Каталог | Что | Namespace |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `base/` | оба коллектора, NetworkPolicy, Namespace | `security-audit` |
|
| `base/` | оба коллектора, NetworkPolicy, AuthorizationPolicy, Namespace | `security-audit` |
|
||||||
|
| `base/idp-actions-bootstrap/` | Job настройки Target/Execution'ов в Zitadel, скрипт и список событий | `security-audit` |
|
||||||
| `mesh/` | EnvoyFilter и Telemetry шлюза; без `namespace` в kustomization, селектор шлюза — заглушка | `istio-system` |
|
| `mesh/` | EnvoyFilter и Telemetry шлюза; без `namespace` в kustomization, селектор шлюза — заглушка | `istio-system` |
|
||||||
| `uralkal/` | оверлей: `base` + `mesh`, селектор шлюза, Kafka, `dependsOn` | — |
|
| `uralkal/` | оверлей: `base` + `mesh`, селектор шлюза, Kafka, `dependsOn` | — |
|
||||||
|
|
||||||
@ -307,6 +309,157 @@ spec:
|
|||||||
недоступности Kafka упрётся в `memory_limiter`/лимит 512Mi раньше, чем в `queue_size`. Записи сверх этого теряются,
|
недоступности Kafka упрётся в `memory_limiter`/лимит 512Mi раньше, чем в `queue_size`. Записи сверх этого теряются,
|
||||||
шлюз не блокируется.
|
шлюз не блокируется.
|
||||||
|
|
||||||
|
## События Zitadel
|
||||||
|
|
||||||
|
Вход, неуспешный вход, блокировки, изменения пользователей, ролей и members (п.14–16 документа ИБ).
|
||||||
|
|
||||||
|
```
|
||||||
|
Zitadel (Actions v2: Execution с условием event → Target restAsync, JSON по HTTP)
|
||||||
|
└─ http://access-collector.security-audit.svc.cluster.local:8088/zitadel/events (mTLS, sidecar у обоих)
|
||||||
|
└─ webhookevent/zitadel ─► transform/idp ─► Kafka security.idp-events ─► processor (infra/k8s-audit, idp/)
|
||||||
|
```
|
||||||
|
|
||||||
|
Target и Execution'ы заводит Job `idp-actions-bootstrap` (`base/idp-actions-bootstrap/`) через API Zitadel.
|
||||||
|
Список событий — `files/events.txt`.
|
||||||
|
|
||||||
|
### Почему без HMAC
|
||||||
|
|
||||||
|
Zitadel подписывает запросы (`X-ZITADEL-Signature`, HMAC-SHA256 тела), но `webhookevent` подпись проверять не умеет.
|
||||||
|
Вместо подписи источник подтверждается на транспортном уровне:
|
||||||
|
- **mTLS Istio:** у Zitadel и коллектора есть sidecar, identity источника — SA `zitadel`;
|
||||||
|
- **AuthorizationPolicy `access-collector`:** на 8088 только `POST /zitadel/events` от `cluster.local/ns/zitadel/sa/zitadel`,
|
||||||
|
на 4317 только из `istio-system`, остальное запрещено (ALLOW-политика закрывает всё, что не разрешено);
|
||||||
|
- **NetworkPolicy:** 8088 только из namespace `zitadel` от подов `app.kubernetes.io/name=idp-contour,component=start`.
|
||||||
|
|
||||||
|
**Условие:** Zitadel в mesh. Сейчас это так: ns `zitadel` с `istio-injection: enabled`, у Deployment метка
|
||||||
|
`sidecar.istio.io/inject: "true"` (чарт `idp-contour` 4.12.13). Если Zitadel выведут из mesh, у его запросов не будет
|
||||||
|
principal, и коллектор начнёт отвечать 403. Тогда либо вернуть sidecar, либо перейти на приёмник с проверкой HMAC
|
||||||
|
(signingKey из `CreateTarget` — во Vault).
|
||||||
|
|
||||||
|
Probe'ы kubelet AuthorizationPolicy не блокирует: при `rewriteAppHTTPProbers` (по умолчанию) sidecar переписывает
|
||||||
|
HTTP-probe на pilot-agent `:15020`, а тот ходит в приложение напрямую, мимо Envoy и RBAC-фильтра. При подключении сбора
|
||||||
|
метрик коллектора (`:8888`) нужно отдельное правило в политике.
|
||||||
|
|
||||||
|
### Поля события
|
||||||
|
|
||||||
|
Тело запроса Zitadel — `ContextInfoEvent` (`internal/repository/execution/queue.go`, v4.12.2), все поля `omitempty`.
|
||||||
|
|
||||||
|
| Поле | Откуда | Примечание |
|
||||||
|
|---|---|---|
|
||||||
|
| `event_type` | Zitadel | например, `user.human.password.check.failed` |
|
||||||
|
| `created_at` | Zitadel | RFC3339Nano |
|
||||||
|
| `aggregateID`, `aggregateType` | Zitadel | объект события (пользователь, грант, организация…) |
|
||||||
|
| `resourceOwner`, `instanceID` | Zitadel | организация и инстанс |
|
||||||
|
| `userID` | Zitadel | кто выполнил действие; нет — системное событие |
|
||||||
|
| `sequence`, `version` | Zitadel | |
|
||||||
|
| `details.user_name` | `event_payload.userName` | только `user.human.added`, `user.human.selfregistered` |
|
||||||
|
| `details.role_keys`, `details.project_id` | `event_payload.roleKeys`, `.projectId` | только `user.grant.*` |
|
||||||
|
| `sarex_kind` | — | `idp_event` |
|
||||||
|
| `sarex_contour` | env `CONTOUR` | |
|
||||||
|
| `sarex_outcome` | по `event_type` | `denied_authn` для `*.check.failed`, иначе `applied` |
|
||||||
|
| `sarex_actor_type` | по `userID` | `human`, без `userID` — `system` |
|
||||||
|
|
||||||
|
**`event_payload` удаляется целиком (п.23).** В payload бывают хеш пароля (`user.human.password.changed`),
|
||||||
|
секреты OTP, client secret, ПДн. Allowlist безопаснее denylist'а: новое поле в новой версии Zitadel не уйдёт в Kafka само.
|
||||||
|
Нужные ИБ поля копируются в `details` явно.
|
||||||
|
- `TODO(verify)`: allowlist стартовый. Уточнить по реальным событиям на стенде, какие ещё поля нужны processor'у.
|
||||||
|
|
||||||
|
Тело, которое не разобралось как JSON, заменяется маркером `{"sarex_kind": "idp_event", "sarex_error": "unparsed"}`:
|
||||||
|
сырой текст мог бы унести payload. В 4.12.2 `event_payload` — объект (`json.RawMessage`); вариант с base64-строкой тоже
|
||||||
|
поддержан.
|
||||||
|
|
||||||
|
### Job `idp-actions-bootstrap`
|
||||||
|
|
||||||
|
- **Образ.** В registry контура нет образа с `curl` и `jq` вместе. init-контейнер из `contour/zitadel/kubectl:1.33.4`
|
||||||
|
(уже нужен setup-Job'у Zitadel) копирует `jq` 1.6 с библиотеками в emptyDir. Скрипт идёт в
|
||||||
|
`contour/kafka-nn/kafka:4.0.0-debian-12-r10` (`curl` 7.88). Оба на Debian 12. `TODO(verify)`: заменить на один
|
||||||
|
toolbox-образ.
|
||||||
|
- **Инстанс Zitadel** выбирается заголовком `x-zitadel-instance-host` (+ `x-zitadel-public-host`, `X-Forwarded-Proto: https`).
|
||||||
|
Это `InstanceHostHeaders`/`PublicHostHeaders` из `defaults.yaml` v4.12.2. `Host` остаётся именем Service: при подмене
|
||||||
|
`Host` на внешний домен istio-proxy не нашёл бы маршрут и отправил бы запрос в passthrough, без mTLS.
|
||||||
|
- **Mesh.** Job в mesh (`sidecar.istio.io/inject=true`, `holdApplicationUntilProxyStarts`). В конце скрипт вызывает
|
||||||
|
`POST 127.0.0.1:15020/quitquitquit`, istio-proxy завершается, под — `Completed`. STRICT mTLS у Zitadel сейчас нет, но
|
||||||
|
Job к нему готов.
|
||||||
|
- **Vault:** роль `security-audit-bootstrap`, `secrets/data/security-audit/zitadel-bootstrap` → `pat` →
|
||||||
|
`/vault/secrets/zitadel-pat`; `excludeOutboundPorts: 8200` — паттерн контура.
|
||||||
|
- **Скрипт** (`files/idp-actions-bootstrap.sh`, POSIX sh) идемпотентен:
|
||||||
|
1. ждёт `/debug/ready` (10 попыток × 15 с);
|
||||||
|
2. печатает все типы событий инстанса (`known: …`, `POST /admin/v1/events/types/_search`);
|
||||||
|
3. ищет Target `security-audit-idp-events` по имени и создаёт или обновляет его (`restAsync`, endpoint, `timeout: 10s`);
|
||||||
|
4. для каждого события — `PUT /v2/actions/executions` с условием `{"event": {"event": "<тип>"}}`. Чужие targets
|
||||||
|
Execution'а сохраняются, свой добавляется в конец;
|
||||||
|
5. неизвестное имя события — ошибка: остальные события обрабатываются, код выхода ≠ 0.
|
||||||
|
|
||||||
|
PAT не печатается (`set -x` нет, заголовки — в файле в tmpfs), signingKey из ответов не выводится.
|
||||||
|
- **Перезапуск.** ConfigMap'ы скрипта и событий — из `configMapGenerator` (хеш в имени). Правка любого файла меняет
|
||||||
|
spec Job'а, и Flux пересоздаёт его (`kustomize.toolkit.fluxcd.io/force: enabled`). После `ttlSecondsAfterFinished` (сутки)
|
||||||
|
Job удаляется, и Flux создаёт его заново: раз в сутки идёт сверка, повторный запуск ничего не меняет.
|
||||||
|
- **Порядок.** У Job нет `dependsOn`: он стартует вместе с HelmRelease'ами. Если Zitadel ещё не готов — до 10 попыток,
|
||||||
|
дальше `backoffLimit: 3`. Execution'ы ссылаются на Target по id, коллектор может подняться позже: до его готовности
|
||||||
|
события теряются (повторов нет, см. риски). Подключать модуль, когда Zitadel уже работает.
|
||||||
|
|
||||||
|
### Предусловия подключения
|
||||||
|
|
||||||
|
- Топик `security.idp-events`: 3 партиции, `retention.ms=259200000`, RF по контуру, тем же путём, что остальные
|
||||||
|
топики. Пользователь `security-audit`, ACL не нужны.
|
||||||
|
- Сервисный пользователь Zitadel `security-audit-bootstrap` (machine user) с ролью `IAM_OWNER` на инстансе.
|
||||||
|
Нужны `action.target.read/write/delete`, `action.execution.read/write`, `events.read`. Его PAT — во Vault
|
||||||
|
`secrets/data/security-audit/zitadel-bootstrap`, ключ `pat`.
|
||||||
|
- Роль Vault `security-audit-bootstrap` в `auth/kubernetes`: bound SA `idp-actions-bootstrap`, namespace `security-audit`,
|
||||||
|
политика `read` на `secrets/data/security-audit/zitadel-bootstrap`.
|
||||||
|
- Feature flag не нужен: в v4.12.2 `actions` в `/v2/features/instance` удалён (reserved), Actions v2 включены всегда.
|
||||||
|
|
||||||
|
Проверка API изнутри кластера (ожидается `"https://login.sarex.local.uralkali.com"`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n zitadel exec deploy/zitadel-idp-contour -c istio-proxy -- \
|
||||||
|
curl -s -H 'x-zitadel-instance-host: login.sarex.local.uralkali.com' -H 'X-Forwarded-Proto: https' \
|
||||||
|
http://zitadel-idp-contour.zitadel.svc.cluster.local:8080/.well-known/openid-configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка после подключения
|
||||||
|
|
||||||
|
- `kubectl -n security-audit logs job/idp-actions-bootstrap -c bootstrap`: `Target …: создан`, по событиям
|
||||||
|
`создано`/`без изменений`, `ошибок=0`. Строки `known:` — сверка имён с инстансом.
|
||||||
|
- Создать тестового пользователя → в `security.idp-events` есть `user.human.added` без `event_payload` и с
|
||||||
|
`details.user_name`.
|
||||||
|
- Неверный пароль → `user.human.password.check.failed`, `sarex_outcome=denied_authn`.
|
||||||
|
- Канарейка в пароле тестового пользователя → её нет в топике (`grep` по выводу `kafka-console-consumer.sh`).
|
||||||
|
- `curl -X POST http://access-collector.security-audit:8088/zitadel/events -d '{}'` из пода в другом namespace → таймаут
|
||||||
|
(NetworkPolicy). Из пода с sidecar в `zitadel`, но не от SA `zitadel` → `403 RBAC: access denied`.
|
||||||
|
|
||||||
|
### Риски
|
||||||
|
|
||||||
|
- **Повторов доставки нет.** Для `restAsync` Zitadel 4.12.2 только пишет ошибку в лог (`internal/execution`): коллектор
|
||||||
|
или Kafka недоступны → событие потеряно. Что смягчает:
|
||||||
|
- 2 реплики, PDB, очередь отправки;
|
||||||
|
- 200 приходит после приёма в `batch`, дальше доставка в Kafka с ретраями.
|
||||||
|
|
||||||
|
Алерты:
|
||||||
|
- `otelcol_exporter_send_failed_log_records{exporter="kafka/idp"}`;
|
||||||
|
- нет `otelcol_receiver_accepted_log_records{receiver="webhookevent/zitadel"}` в рабочее время.
|
||||||
|
- **Полный источник — Events API Zitadel** (`POST /admin/v1/events/_search`). Суточную сверку количества по типам можно
|
||||||
|
добавить позже.
|
||||||
|
- **Размер тела.** `webhookevent` 0.121 читает тело одним `bufio.Scanner` без явного буфера: событие больше ~64 КиБ,
|
||||||
|
вероятно, отбрасывается. Ответ при этом 200. `TODO(verify)` на стенде; события пользователей обычно в разы меньше.
|
||||||
|
- В 0.121 у `webhookevent` нет `header_attribute_regex`, есть только `required_header`: заголовки в запись не попадают.
|
||||||
|
|
||||||
|
### Откат
|
||||||
|
|
||||||
|
Снять Target со всех Execution'ов из списка и удалить его — скрипт с `--remove`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n security-audit get job idp-actions-bootstrap -o json \
|
||||||
|
| jq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.labels,
|
||||||
|
.metadata.annotations, .metadata.ownerReferences, .spec.selector, .status)
|
||||||
|
| .spec.template.metadata.labels |= with_entries(select(.key | test("controller-uid|job-name") | not))
|
||||||
|
| .metadata.name = "idp-actions-remove" | .spec.template.spec.containers[0].args = ["--remove"]' \
|
||||||
|
| kubectl create -f -
|
||||||
|
```
|
||||||
|
|
||||||
|
Пока модуль подключён, Flux вернёт Execution'ы при следующем пересоздании Job'а. Для постоянного отключения убрать
|
||||||
|
`idp-actions-bootstrap` из `base/kustomization.yaml` или модуль из кластера.
|
||||||
|
|
||||||
## Проверить перед подключением
|
## Проверить перед подключением
|
||||||
|
|
||||||
Сверено по репозиторию и чартам 01.11.2026, доступа к кластеру не было.
|
Сверено по репозиторию и чартам 01.11.2026, доступа к кластеру не было.
|
||||||
@ -331,6 +484,16 @@ spec:
|
|||||||
| Образ access-collector | тот же; `otlp` receiver, `transform`, `kafka` exporter есть | `otelcol-contrib validate` — OK |
|
| Образ access-collector | тот же; `otlp` receiver, `transform`, `kafka` exporter есть | `otelcol-contrib validate` — OK |
|
||||||
| Логика `transform/access` | 7 записей в формате Envoy ALS: `-` → пусто, `token/***`, `***jwt***`, классы верные | локальный коллектор, OTLP/HTTP → `file`-экспортёр |
|
| Логика `transform/access` | 7 записей в формате Envoy ALS: `-` → пусто, `token/***`, `***jwt***`, классы верные | локальный коллектор, OTLP/HTTP → `file`-экспортёр |
|
||||||
| Схемы Istio | EnvoyFilter (`v1alpha3`) и Telemetry (`v1`) проходят CRD-схемы `istio-base-contour` 1.24.0 | свой проверщик по `openAPIV3Schema`; `istioctl`/`kubeconform` на машине нет. Конфиг Lua (`patch.value`) схемой не проверяется |
|
| Схемы Istio | EnvoyFilter (`v1alpha3`) и Telemetry (`v1`) проходят CRD-схемы `istio-base-contour` 1.24.0 | свой проверщик по `openAPIV3Schema`; `istioctl`/`kubeconform` на машине нет. Конфиг Lua (`patch.value`) схемой не проверяется |
|
||||||
|
| **События Zitadel** (02.11.2026) | | |
|
||||||
|
| Zitadel в mesh | ns `zitadel` `istio-injection: enabled`, у Deployment `sidecar.istio.io/inject: "true"`, SA `zitadel`, метки `app.kubernetes.io/name=idp-contour`, `component=start`; trust domain `cluster.local` | `helm template` `idp-contour` 4.12.13 с values `uralkal`, mesh config istiod |
|
||||||
|
| Версия Zitadel | образ `contour/zitadel/zitadel:4.12.2` | `values.yaml` чарта |
|
||||||
|
| Actions v2 API | `POST /v2/actions/targets`, `POST /v2/actions/targets/{id}` (update), `POST …/targets/search` (`targetNameFilter`), `PUT /v2/actions/executions` (`targets` — список id), `POST …/executions/search` (`inConditionsFilter`); условие `event.event`/`event.group`/`event.all`; feature flag не нужен | proto `zitadel/action/v2`, `feature/v2` тега v4.12.2 |
|
||||||
|
| Формат события | `ContextInfoEvent`, `event_payload` — `json.RawMessage`; повторов для async нет | `internal/repository/execution/queue.go`, `internal/execution/*.go` v4.12.2 |
|
||||||
|
| Имена событий | все 58 из `events.txt` есть в `internal/repository/{user,usergrant,org,project,instance,member}` v4.12.2; агрегата `group` нет | исходники; окончательно — строки `known:` в логе Job'а |
|
||||||
|
| `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 контура |
|
||||||
|
| Скрипт | `sh -n` dash/bash --posix/busybox — OK; mock: создание, повтор без изменений, объединение с чужим target, обновление endpoint, `--remove`, API недоступен, 401; утечек PAT/signingKey нет | mock Zitadel на Python, запуск в образе kafka с `--read-only`, uid 1001. shellcheck на машине нет |
|
||||||
|
|
||||||
Остаётся открытым:
|
Остаётся открытым:
|
||||||
|
|
||||||
@ -342,5 +505,9 @@ spec:
|
|||||||
- `set(body, attributes)` + `encoding: raw` даёт JSON-объект (проверено локально через `file`-экспортёр, raw-маршалер
|
- `set(body, attributes)` + `encoding: raw` даёт JSON-объект (проверено локально через `file`-экспортёр, raw-маршалер
|
||||||
сериализует map в JSON) — подтвердить на стенде чтением топика. Там же проверить, что Envoy шлёт labels строками
|
сериализует map в JSON) — подтвердить на стенде чтением топика. Там же проверить, что Envoy шлёт labels строками
|
||||||
(сравнения `response_code == "401"`).
|
(сравнения `response_code == "401"`).
|
||||||
|
- `TODO(verify)` **API Zitadel изнутри кластера** с `x-zitadel-instance-host` (команда в «События Zitadel»): по
|
||||||
|
исходникам должно работать, запрос не выполнялся.
|
||||||
|
- `TODO(verify)` **allowlist полей** `details` по реальным событиям; **лимит ~64 КиБ** тела у `webhookevent`.
|
||||||
|
- `TODO(verify)` **toolbox-образ** для Job вместо пары kubectl + kafka.
|
||||||
- Некритичные предупреждения 0.121.0: `service::telemetry::metrics::address` устарел (так же во всех коллекторах репо);
|
- Некритичные предупреждения 0.121.0: `service::telemetry::metrics::address` устарел (так же во всех коллекторах репо);
|
||||||
OTTL-пути без префикса `log.` (контекст выводится автоматически).
|
OTTL-пути без префикса `log.` (контекст выводится автоматически).
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
# Транспортная аутентификация вместо HMAC-подписи Zitadel: mTLS Istio + проверка identity источника.
|
||||||
|
# Как только к workload применяется хотя бы одна ALLOW-политика, весь остальной трафик к нему запрещён.
|
||||||
|
# - Probe'ы kubelet не блокируются: sidecar переписывает их на pilot-agent (:15020), а тот обращается
|
||||||
|
# к приложению напрямую, мимо Envoy и его RBAC-фильтра.
|
||||||
|
# - При подключении сбора метрик коллектора (:8888) добавить отдельное правило для источника сбора.
|
||||||
|
# Условие: Zitadel в mesh (ns zitadel istio-injection=enabled, под с sidecar.istio.io/inject=true), иначе
|
||||||
|
# у его запросов нет principal и правило 1 их отклонит.
|
||||||
|
apiVersion: security.istio.io/v1
|
||||||
|
kind: AuthorizationPolicy
|
||||||
|
metadata:
|
||||||
|
name: access-collector
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
security-audit/collector: access # podLabels HelmRelease access-collector
|
||||||
|
action: ALLOW
|
||||||
|
rules:
|
||||||
|
- from:
|
||||||
|
- source:
|
||||||
|
principals: ["cluster.local/ns/zitadel/sa/zitadel"] # SA Deployment zitadel-idp-contour
|
||||||
|
to:
|
||||||
|
- operation: {ports: ["8088"], methods: ["POST"], paths: ["/zitadel/events"]}
|
||||||
|
- from:
|
||||||
|
- source:
|
||||||
|
namespaces: ["istio-system"]
|
||||||
|
to:
|
||||||
|
- operation: {ports: ["4317"]}
|
||||||
@ -70,6 +70,13 @@ spec:
|
|||||||
servicePort: 4317
|
servicePort: 4317
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
appProtocol: grpc # Istio выбирает протокол по appProtocol
|
appProtocol: grpc # Istio выбирает протокол по appProtocol
|
||||||
|
# События Zitadel Actions v2 (Target restAsync) — обычный JSON по HTTP, приёмник webhookevent.
|
||||||
|
zitadel-events:
|
||||||
|
enabled: true
|
||||||
|
containerPort: 8088
|
||||||
|
servicePort: 8088
|
||||||
|
protocol: TCP
|
||||||
|
appProtocol: http
|
||||||
otlp-http: {enabled: false}
|
otlp-http: {enabled: false}
|
||||||
jaeger-compact: {enabled: false}
|
jaeger-compact: {enabled: false}
|
||||||
jaeger-thrift: {enabled: false}
|
jaeger-thrift: {enabled: false}
|
||||||
@ -140,6 +147,12 @@ spec:
|
|||||||
grpc:
|
grpc:
|
||||||
endpoint: "${env:MY_POD_IP}:4317"
|
endpoint: "${env:MY_POD_IP}:4317"
|
||||||
max_recv_msg_size_mib: 16
|
max_recv_msg_size_mib: 16
|
||||||
|
# Подпись HMAC (X-ZITADEL-Signature) не проверяется: доступ ограничен mTLS + AuthorizationPolicy
|
||||||
|
# (только SA zitadel) + NetworkPolicy. Отвечает 200 после ConsumeLogs, т. е. после приёма в batch.
|
||||||
|
webhookevent/zitadel:
|
||||||
|
endpoint: "${env:MY_POD_IP}:8088"
|
||||||
|
path: /zitadel/events
|
||||||
|
health_path: /zitadel/health
|
||||||
processors:
|
processors:
|
||||||
memory_limiter: {check_interval: 1s, limit_mib: 400, spike_limit_mib: 100}
|
memory_limiter: {check_interval: 1s, limit_mib: 400, spike_limit_mib: 100}
|
||||||
transform/access:
|
transform/access:
|
||||||
@ -167,20 +180,54 @@ spec:
|
|||||||
- set(body["sarex_outcome"], "error") where Int(body["response_code"]) >= 500 or body["response_code"] == "0"
|
- set(body["sarex_outcome"], "error") where Int(body["response_code"]) >= 500 or body["response_code"] == "0"
|
||||||
- set(body["sarex_actor_type"], "anonymous")
|
- set(body["sarex_actor_type"], "anonymous")
|
||||||
- set(body["sarex_actor_type"], "human") where body["user_sub"] != ""
|
- set(body["sarex_actor_type"], "human") where body["user_sub"] != ""
|
||||||
|
transform/idp:
|
||||||
|
error_mode: ignore
|
||||||
|
log_statements:
|
||||||
|
- context: log
|
||||||
|
statements:
|
||||||
|
- set(body, ParseJSON(body))
|
||||||
|
# Не JSON — тело целиком заменяется маркером: сырой текст мог бы унести event_payload в Kafka (п.23).
|
||||||
|
- 'set(body, {"sarex_kind": "idp_event", "sarex_error": "unparsed"}) where not IsMap(body)'
|
||||||
|
# В 4.12.2 event_payload — json.RawMessage (объект); base64-строку тоже поддерживаем
|
||||||
|
- set(body["event_payload"], ParseJSON(Decode(body["event_payload"], "base64"))) where IsString(body["event_payload"])
|
||||||
|
# allowlist (п.23): нужные ИБ поля копируются явно, payload удаляется ЦЕЛИКОМ
|
||||||
|
# (в событиях бывают хеши паролей, OTP-секреты, client secret, ПДн)
|
||||||
|
# TODO(verify): список полей по типам событий уточнить по реальным событиям на стенде
|
||||||
|
- set(body["details"]["user_name"], body["event_payload"]["userName"]) where IsMatch(body["event_type"], "^user\\.human\\.(added|selfregistered)$")
|
||||||
|
- set(body["details"]["role_keys"], body["event_payload"]["roleKeys"]) where IsMatch(body["event_type"], "^user\\.grant\\.")
|
||||||
|
- set(body["details"]["project_id"], body["event_payload"]["projectId"]) where IsMatch(body["event_type"], "^user\\.grant\\.")
|
||||||
|
- delete_key(body, "event_payload")
|
||||||
|
# обогащение и классификация
|
||||||
|
- set(body["sarex_kind"], "idp_event")
|
||||||
|
- set(body["sarex_contour"], "${env:CONTOUR}")
|
||||||
|
- set(body["sarex_outcome"], "applied")
|
||||||
|
- set(body["sarex_outcome"], "denied_authn") where IsMatch(body["event_type"], "check\\.failed$")
|
||||||
|
- set(body["sarex_actor_type"], "human")
|
||||||
|
- set(body["sarex_actor_type"], "system") where body["userID"] == "" or body["userID"] == nil
|
||||||
batch: {send_batch_size: 1000, send_batch_max_size: 2000, timeout: 2s}
|
batch: {send_batch_size: 1000, send_batch_max_size: 2000, timeout: 2s}
|
||||||
exporters:
|
exporters:
|
||||||
|
# Общие поля Kafka — YAML-якоря (&kafka_*), kafka/idp ссылается на них (*kafka_*).
|
||||||
kafka/access:
|
kafka/access:
|
||||||
brokers: ["${env:KAFKA_BROKERS}"]
|
brokers: &kafka_brokers ["${env:KAFKA_BROKERS}"]
|
||||||
protocol_version: 2.8.0
|
protocol_version: &kafka_protocol_version 2.8.0
|
||||||
topic: security.mesh-access
|
topic: security.mesh-access
|
||||||
encoding: raw # в сообщение — только body (JSON записи), без OTLP-обёртки
|
encoding: raw # в сообщение — только body (JSON записи), без OTLP-обёртки
|
||||||
auth:
|
auth: &kafka_auth
|
||||||
sasl: {mechanism: SCRAM-SHA-512, username: "${file:/vault/secrets/kafka-username}", password: "${file:/vault/secrets/kafka-password}"}
|
sasl: {mechanism: SCRAM-SHA-512, username: "${file:/vault/secrets/kafka-username}", password: "${file:/vault/secrets/kafka-password}"}
|
||||||
tls: {insecure: false, ca_file: /vault/secrets/kafka-ca.crt}
|
tls: {insecure: false, ca_file: /vault/secrets/kafka-ca.crt}
|
||||||
producer: {required_acks: -1, compression: zstd, max_message_bytes: 1048576}
|
producer: &kafka_producer {required_acks: -1, compression: zstd, max_message_bytes: 1048576}
|
||||||
# Очередь в памяти (диска у Deployment нет): при падении пода неотправленные записи теряются.
|
# Очередь в памяти (диска у Deployment нет): при падении пода неотправленные записи теряются.
|
||||||
sending_queue: {enabled: true, queue_size: 50000, num_consumers: 4}
|
sending_queue: &kafka_sending_queue {enabled: true, queue_size: 50000, num_consumers: 4}
|
||||||
retry_on_failure: {enabled: true, initial_interval: 5s, max_interval: 60s, max_elapsed_time: 0}
|
retry_on_failure: &kafka_retry {enabled: true, initial_interval: 5s, max_interval: 60s, max_elapsed_time: 0}
|
||||||
|
kafka/idp:
|
||||||
|
brokers: *kafka_brokers
|
||||||
|
protocol_version: *kafka_protocol_version
|
||||||
|
topic: security.idp-events
|
||||||
|
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:
|
||||||
@ -188,6 +235,10 @@ spec:
|
|||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [memory_limiter, transform/access, batch]
|
processors: [memory_limiter, transform/access, batch]
|
||||||
exporters: [kafka/access]
|
exporters: [kafka/access]
|
||||||
|
logs/idp:
|
||||||
|
receivers: [webhookevent/zitadel]
|
||||||
|
processors: [memory_limiter, transform/idp, batch]
|
||||||
|
exporters: [kafka/idp]
|
||||||
telemetry:
|
telemetry:
|
||||||
logs: {level: info}
|
logs: {level: info}
|
||||||
metrics: {address: "${env:MY_POD_IP}:8888"}
|
metrics: {address: "${env:MY_POD_IP}:8888"}
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
# Типы событий Zitadel, на которые вешается Execution → access-collector (п.14–16 документа ИБ).
|
||||||
|
# По одному в строке, # — комментарий. Имена сверены с internal/repository/* Zitadel v4.12.2;
|
||||||
|
# фактический список инстанса Job печатает в лог (known: ...), неизвестное имя — ошибка Job'а.
|
||||||
|
# Групп пользователей (агрегат group) в v4.12.2 нет.
|
||||||
|
|
||||||
|
# создание пользователя
|
||||||
|
user.human.added
|
||||||
|
user.human.selfregistered
|
||||||
|
user.machine.added
|
||||||
|
# изменение пользователя
|
||||||
|
user.human.profile.changed
|
||||||
|
user.human.email.changed
|
||||||
|
user.human.phone.changed
|
||||||
|
user.username.changed
|
||||||
|
user.machine.changed
|
||||||
|
user.human.password.changed
|
||||||
|
# жизненный цикл: деактивация, блокировка, удаление
|
||||||
|
user.deactivated
|
||||||
|
user.reactivated
|
||||||
|
user.locked
|
||||||
|
user.unlocked
|
||||||
|
user.removed
|
||||||
|
# вход: пароль
|
||||||
|
user.human.password.check.succeeded
|
||||||
|
user.human.password.check.failed
|
||||||
|
# вход: второй фактор
|
||||||
|
user.human.mfa.otp.check.succeeded
|
||||||
|
user.human.mfa.otp.check.failed
|
||||||
|
user.human.mfa.otp.sms.check.succeeded
|
||||||
|
user.human.mfa.otp.sms.check.failed
|
||||||
|
user.human.mfa.otp.email.check.succeeded
|
||||||
|
user.human.mfa.otp.email.check.failed
|
||||||
|
user.human.mfa.u2f.token.check.succeeded
|
||||||
|
user.human.mfa.u2f.token.check.failed
|
||||||
|
user.human.passwordless.token.check.succeeded
|
||||||
|
user.human.passwordless.token.check.failed
|
||||||
|
# изменение факторов и токенов (добавление/снятие второго фактора, PAT, имперсонация)
|
||||||
|
user.human.mfa.otp.added
|
||||||
|
user.human.mfa.otp.removed
|
||||||
|
user.human.mfa.u2f.token.added
|
||||||
|
user.human.mfa.u2f.token.removed
|
||||||
|
user.human.passwordless.token.added
|
||||||
|
user.human.passwordless.token.removed
|
||||||
|
user.token.added
|
||||||
|
user.token.removed
|
||||||
|
user.impersonated
|
||||||
|
# роли (user grants)
|
||||||
|
user.grant.added
|
||||||
|
user.grant.changed
|
||||||
|
user.grant.cascade.changed
|
||||||
|
user.grant.removed
|
||||||
|
user.grant.cascade.removed
|
||||||
|
user.grant.deactivated
|
||||||
|
user.grant.reactivated
|
||||||
|
# members: инстанс (администраторы IAM), организация, проект, грант проекта
|
||||||
|
instance.member.added
|
||||||
|
instance.member.changed
|
||||||
|
instance.member.removed
|
||||||
|
instance.member.cascade.removed
|
||||||
|
org.member.added
|
||||||
|
org.member.changed
|
||||||
|
org.member.removed
|
||||||
|
org.member.cascade.removed
|
||||||
|
project.member.added
|
||||||
|
project.member.changed
|
||||||
|
project.member.removed
|
||||||
|
project.member.cascade.removed
|
||||||
|
project.grant.member.added
|
||||||
|
project.grant.member.changed
|
||||||
|
project.grant.member.removed
|
||||||
|
project.grant.member.cascade.removed
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Идемпотентная настройка Zitadel Actions v2 для журнала ИБ: Target (restAsync) → access-collector
|
||||||
|
# и Execution'ы на события из EVENTS_FILE. С флагом --remove (или MODE=remove) снимает Target
|
||||||
|
# с этих Execution'ов и удаляет его.
|
||||||
|
#
|
||||||
|
# Без set -x: заголовки с PAT лежат в файле $TMP/headers и в вывод не попадают.
|
||||||
|
# Ответы API с signingKey наружу не выводятся: коллектор подпись не проверяет. При переходе на приёмник
|
||||||
|
# с проверкой HMAC ключ из ответа CreateTarget нужно будет записать во Vault, а не в лог.
|
||||||
|
#
|
||||||
|
# Код выхода ≠ 0 при любой ошибке; остальные события при этом всё равно обрабатываются.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
MODE="${MODE:-apply}"
|
||||||
|
[ "${1:-}" = "--remove" ] && MODE=remove
|
||||||
|
|
||||||
|
: "${ZITADEL_API:?ZITADEL_API не задан (http://<service>:8080)}"
|
||||||
|
: "${ZITADEL_HOST:?ZITADEL_HOST не задан (внешний домен инстанса, ExternalDomain)}"
|
||||||
|
TARGET_NAME="${TARGET_NAME:-security-audit-idp-events}"
|
||||||
|
TARGET_ENDPOINT="${TARGET_ENDPOINT:-http://access-collector.security-audit.svc.cluster.local:8088/zitadel/events}"
|
||||||
|
TARGET_TIMEOUT="${TARGET_TIMEOUT:-10s}"
|
||||||
|
EVENTS_FILE="${EVENTS_FILE:-/config/events.txt}"
|
||||||
|
PAT_FILE="${PAT_FILE:-/vault/secrets/zitadel-pat}"
|
||||||
|
CURL_MAX_TIME="${CURL_MAX_TIME:-20}"
|
||||||
|
CONNECT_ATTEMPTS="${CONNECT_ATTEMPTS:-10}"
|
||||||
|
CONNECT_PAUSE="${CONNECT_PAUSE:-15}"
|
||||||
|
ISTIO_QUIT_ON_EXIT="${ISTIO_QUIT_ON_EXIT:-false}"
|
||||||
|
TMP="${TMPDIR:-/tmp}"
|
||||||
|
|
||||||
|
# Job в mesh: istio-proxy сам не завершится, без quitquitquit под останется Running.
|
||||||
|
quit_proxy() {
|
||||||
|
if [ "$ISTIO_QUIT_ON_EXIT" = "true" ]; then
|
||||||
|
curl -fsS --max-time 5 -X POST http://127.0.0.1:15020/quitquitquit >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap quit_proxy EXIT
|
||||||
|
|
||||||
|
log() { printf '%s\n' "$*"; }
|
||||||
|
err() { printf 'ОШИБКА: %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
[ -r "$PAT_FILE" ] || { err "нет файла PAT $PAT_FILE (Vault Agent)"; exit 1; }
|
||||||
|
[ -r "$EVENTS_FILE" ] || { err "нет списка событий $EVENTS_FILE"; exit 1; }
|
||||||
|
|
||||||
|
# Инстанс выбирается по x-zitadel-instance-host (InstanceHostHeaders Zitadel), Host остаётся именем Service:
|
||||||
|
# по нему istio-proxy находит маршрут и шлёт запрос с mTLS. X-Forwarded-Proto — инстанс с ExternalSecure.
|
||||||
|
umask 077
|
||||||
|
{
|
||||||
|
printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' <"$PAT_FILE")"
|
||||||
|
printf 'x-zitadel-instance-host: %s\n' "$ZITADEL_HOST"
|
||||||
|
printf 'x-zitadel-public-host: %s\n' "$ZITADEL_HOST"
|
||||||
|
printf 'X-Forwarded-Proto: https\n'
|
||||||
|
printf 'Content-Type: application/json\n'
|
||||||
|
printf 'Accept: application/json\n'
|
||||||
|
} >"$TMP/headers"
|
||||||
|
|
||||||
|
# api METHOD PATH [BODY]: тело ответа в stdout при 2xx; иначе код и текст ошибки в stderr, return 1.
|
||||||
|
api() {
|
||||||
|
_method=$1 _path=$2
|
||||||
|
if [ $# -ge 3 ]; then set -- --data "$3"; else set --; fi
|
||||||
|
_code=$(curl -sS -o "$TMP/resp" -w '%{http_code}' --max-time "$CURL_MAX_TIME" \
|
||||||
|
-X "$_method" -H @"$TMP/headers" "$@" "$ZITADEL_API$_path") || _code=000
|
||||||
|
case "$_code" in
|
||||||
|
2??) cat "$TMP/resp" ;;
|
||||||
|
*) err "$_method $_path → HTTP $_code: $(head -c 300 "$TMP/resp" 2>/dev/null | tr '\n' ' ')"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. Доступность API (Zitadel и коллектор поднимаются раньше Job'а, но без dependsOn) ---
|
||||||
|
attempt=1
|
||||||
|
until curl -fsS -o /dev/null --max-time "$CURL_MAX_TIME" -H @"$TMP/headers" "$ZITADEL_API/debug/ready"; do
|
||||||
|
if [ "$attempt" -ge "$CONNECT_ATTEMPTS" ]; then
|
||||||
|
err "Zitadel недоступен: $ZITADEL_API (попыток: $attempt)"; exit 1
|
||||||
|
fi
|
||||||
|
log "Zitadel не готов, попытка $attempt/$CONNECT_ATTEMPTS, пауза ${CONNECT_PAUSE}s"
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep "$CONNECT_PAUSE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- 2. Типы событий инстанса: в лог — для сверки списка, и для проверки имён ниже ---
|
||||||
|
# Ответ сначала в переменную: в конвейере api | jq код ошибки api потерялся бы (set -e смотрит на jq).
|
||||||
|
resp=$(api POST /admin/v1/events/types/_search '{}')
|
||||||
|
printf '%s' "$resp" | jq -r '.eventTypes[]?.type' | sort -u >"$TMP/known-events"
|
||||||
|
log "Типы событий Zitadel ($(wc -l <"$TMP/known-events")):"
|
||||||
|
sed 's/^/ known: /' "$TMP/known-events"
|
||||||
|
|
||||||
|
sed -e 's/#.*//' -e 's/[[:space:]]//g' "$EVENTS_FILE" | grep -v '^$' | sort -u >"$TMP/events"
|
||||||
|
|
||||||
|
# --- 3. Target ---
|
||||||
|
search=$(jq -nc --arg n "$TARGET_NAME" \
|
||||||
|
'{filters: [{targetNameFilter: {targetName: $n, method: "TEXT_FILTER_METHOD_EQUALS"}}]}')
|
||||||
|
resp=$(api POST /v2/actions/targets/search "$search")
|
||||||
|
printf '%s' "$resp" | jq -c --arg n "$TARGET_NAME" '[.targets[]? | select(.name == $n)
|
||||||
|
| {id, endpoint, timeout, async: has("restAsync")}]' >"$TMP/targets"
|
||||||
|
found=$(jq length "$TMP/targets")
|
||||||
|
if [ "$found" -gt 1 ]; then
|
||||||
|
err "Target'ов с именем $TARGET_NAME: $found, ожидался один — разберитесь вручную"; exit 1
|
||||||
|
fi
|
||||||
|
target_id=$(jq -r '.[0].id // empty' "$TMP/targets")
|
||||||
|
|
||||||
|
if [ "$MODE" = "apply" ]; then
|
||||||
|
want=$(jq -nc --arg e "$TARGET_ENDPOINT" --arg t "$TARGET_TIMEOUT" \
|
||||||
|
'{restAsync: {}, endpoint: $e, timeout: $t}')
|
||||||
|
if [ -z "$target_id" ]; then
|
||||||
|
# Из ответа берётся только id: signingKey не выводится и не сохраняется.
|
||||||
|
resp=$(api POST /v2/actions/targets "$(printf '%s' "$want" | jq -c --arg n "$TARGET_NAME" '. + {name: $n}')")
|
||||||
|
target_id=$(printf '%s' "$resp" | jq -r '.id // empty')
|
||||||
|
[ -n "$target_id" ] || { err "CreateTarget не вернул id"; exit 1; }
|
||||||
|
log "Target $TARGET_NAME: создан, id=$target_id"
|
||||||
|
elif jq -e --arg e "$TARGET_ENDPOINT" --arg t "$TARGET_TIMEOUT" \
|
||||||
|
'.[0] | .async and .endpoint == $e and .timeout == $t' "$TMP/targets" >/dev/null; then
|
||||||
|
log "Target $TARGET_NAME: без изменений, id=$target_id"
|
||||||
|
else
|
||||||
|
api POST "/v2/actions/targets/$target_id" "$want" >/dev/null
|
||||||
|
log "Target $TARGET_NAME: обновлён (тип/endpoint/timeout), id=$target_id"
|
||||||
|
fi
|
||||||
|
elif [ -z "$target_id" ]; then
|
||||||
|
log "Target $TARGET_NAME не найден — снимать нечего"; exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. Execution'ы: чужие targets сохраняются, свой добавляется в конец (или убирается при --remove) ---
|
||||||
|
rc=0; n_created=0; n_extended=0; n_same=0; n_removed=0; n_err=0
|
||||||
|
while IFS= read -r ev; do
|
||||||
|
if ! grep -qxF "$ev" "$TMP/known-events"; then
|
||||||
|
log " $ev: ошибка — такого типа нет в Zitadel"; n_err=$((n_err + 1)); rc=1; continue
|
||||||
|
fi
|
||||||
|
query=$(jq -nc --arg e "$ev" '{filters: [{inConditionsFilter: {conditions: [{event: {event: $e}}]}}]}')
|
||||||
|
if ! resp=$(api POST /v2/actions/executions/search "$query"); then
|
||||||
|
log " $ev: ошибка чтения"; n_err=$((n_err + 1)); rc=1; continue
|
||||||
|
fi
|
||||||
|
current=$(printf '%s' "$resp" \
|
||||||
|
| jq -c --arg e "$ev" '[.executions[]? | select(.condition.event.event == $e) | .targets[]?]')
|
||||||
|
has=$(printf '%s' "$current" | jq --arg t "$target_id" 'index($t) != null')
|
||||||
|
if [ "$MODE" = "apply" ]; then
|
||||||
|
if [ "$has" = "true" ]; then log " $ev: без изменений"; n_same=$((n_same + 1)); continue; fi
|
||||||
|
targets=$(printf '%s' "$current" | jq -c --arg t "$target_id" '. + [$t]')
|
||||||
|
if [ "$current" = "[]" ]; then status="создано"; else status="дополнено (были другие targets)"; fi
|
||||||
|
else
|
||||||
|
if [ "$has" != "true" ]; then log " $ev: без изменений"; n_same=$((n_same + 1)); continue; fi
|
||||||
|
targets=$(printf '%s' "$current" | jq -c --arg t "$target_id" 'map(select(. != $t))')
|
||||||
|
status="target снят"
|
||||||
|
fi
|
||||||
|
body=$(jq -nc --arg e "$ev" --argjson t "$targets" '{condition: {event: {event: $e}}, targets: $t}')
|
||||||
|
if api PUT /v2/actions/executions "$body" >/dev/null; then
|
||||||
|
log " $ev: $status"
|
||||||
|
case "$status" in
|
||||||
|
создано) n_created=$((n_created + 1)) ;;
|
||||||
|
дополнено*) n_extended=$((n_extended + 1)) ;;
|
||||||
|
*) n_removed=$((n_removed + 1)) ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
log " $ev: ошибка записи"; n_err=$((n_err + 1)); rc=1
|
||||||
|
fi
|
||||||
|
done <"$TMP/events"
|
||||||
|
|
||||||
|
if [ "$MODE" = "remove" ] && [ "$rc" -eq 0 ]; then
|
||||||
|
api DELETE "/v2/actions/targets/$target_id" >/dev/null && log "Target $TARGET_NAME удалён, id=$target_id" || rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Итог ($MODE): target=$target_id, событий=$(wc -l <"$TMP/events"), создано=$n_created, дополнено=$n_extended, снято=$n_removed, без изменений=$n_same, ошибок=$n_err"
|
||||||
|
exit "$rc"
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
# Один раз (и после каждой правки скрипта/списка событий) настраивает Target и Execution'ы в Zitadel.
|
||||||
|
# Spec Job'а неизменяем: Flux пересоздаёт его (force). После ttlSecondsAfterFinished Job удаляется,
|
||||||
|
# и Flux создаёт его заново при следующей сверке — скрипт идемпотентен, повторный запуск ничего не меняет.
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: idp-actions-bootstrap
|
||||||
|
annotations:
|
||||||
|
kustomize.toolkit.fluxcd.io/force: enabled
|
||||||
|
spec:
|
||||||
|
backoffLimit: 3
|
||||||
|
ttlSecondsAfterFinished: 86400
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
security-audit/job: idp-actions-bootstrap
|
||||||
|
# В mesh: запросы к Zitadel идут с mTLS (п.30). istio-proxy завершает сам скрипт (quitquitquit).
|
||||||
|
sidecar.istio.io/inject: "true"
|
||||||
|
annotations:
|
||||||
|
sidecar.istio.io/inject: "true"
|
||||||
|
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||||
|
traffic.sidecar.istio.io/excludeOutboundPorts: "8200" # Vault мимо istio-proxy (паттерн контура)
|
||||||
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
|
vault.hashicorp.com/agent-init-first: "true"
|
||||||
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||||
|
vault.hashicorp.com/agent-requests-cpu: "25m"
|
||||||
|
vault.hashicorp.com/agent-limits-cpu: "100m"
|
||||||
|
vault.hashicorp.com/auth-path: auth/kubernetes
|
||||||
|
vault.hashicorp.com/role: security-audit-bootstrap
|
||||||
|
vault.hashicorp.com/agent-inject-secret-zitadel-pat: secrets/data/security-audit/zitadel-bootstrap
|
||||||
|
vault.hashicorp.com/agent-inject-template-zitadel-pat: |-
|
||||||
|
{{- with secret "secrets/data/security-audit/zitadel-bootstrap" -}}
|
||||||
|
{{ .Data.data.pat }}
|
||||||
|
{{- end -}}
|
||||||
|
spec:
|
||||||
|
serviceAccountName: idp-actions-bootstrap
|
||||||
|
restartPolicy: Never
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: regcred
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
seccompProfile: {type: RuntimeDefault}
|
||||||
|
initContainers:
|
||||||
|
# В registry контура нет образа с curl и jq вместе: jq (1.6, Debian 12) берётся из образа kubectl,
|
||||||
|
# который уже использует setup-Job Zitadel, curl — из образа kafka (тоже Debian 12).
|
||||||
|
# TODO(verify): заменить на один toolbox-образ (generic/base-image), когда он появится.
|
||||||
|
- name: tools
|
||||||
|
image: cr.yandex/crp3ccidau046kdj8g9q/contour/zitadel/kubectl:1.33.4
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- cp -L /usr/bin/jq /lib/x86_64-linux-gnu/libjq.so.1 /lib/x86_64-linux-gnu/libonig.so.5 /tools/
|
||||||
|
securityContext: &container-security
|
||||||
|
runAsNonRoot: true
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
resources: &resources
|
||||||
|
requests: {cpu: 10m, memory: 32Mi}
|
||||||
|
limits: {cpu: 200m, memory: 128Mi}
|
||||||
|
volumeMounts:
|
||||||
|
- {name: tools, mountPath: /tools}
|
||||||
|
containers:
|
||||||
|
- name: bootstrap
|
||||||
|
image: cr.yandex/crp3ccidau046kdj8g9q/contour/kafka-nn/kafka:4.0.0-debian-12-r10
|
||||||
|
command: ["sh", "/scripts/idp-actions-bootstrap.sh"]
|
||||||
|
env:
|
||||||
|
# ZITADEL_API и ZITADEL_HOST задаёт оверлей контура.
|
||||||
|
- {name: PATH, value: "/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}
|
||||||
|
- {name: LD_LIBRARY_PATH, value: /tools}
|
||||||
|
- {name: EVENTS_FILE, value: /config/events.txt}
|
||||||
|
- {name: PAT_FILE, value: /vault/secrets/zitadel-pat}
|
||||||
|
- {name: ISTIO_QUIT_ON_EXIT, value: "true"}
|
||||||
|
- {name: TMPDIR, value: /tmp}
|
||||||
|
securityContext: *container-security
|
||||||
|
resources: *resources
|
||||||
|
volumeMounts:
|
||||||
|
- {name: tools, mountPath: /tools, readOnly: true}
|
||||||
|
- {name: script, mountPath: /scripts, readOnly: true}
|
||||||
|
- {name: events, mountPath: /config, readOnly: true}
|
||||||
|
- {name: tmp, mountPath: /tmp}
|
||||||
|
volumes:
|
||||||
|
- {name: tools, emptyDir: {sizeLimit: 16Mi}}
|
||||||
|
- {name: tmp, emptyDir: {medium: Memory, sizeLimit: 16Mi}} # заголовки с PAT — только в памяти
|
||||||
|
- name: script
|
||||||
|
configMap: {name: idp-actions-bootstrap-script}
|
||||||
|
- name: events
|
||||||
|
configMap: {name: idp-actions-bootstrap-events}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
# Настройка Zitadel Actions v2 (Target + Execution'ы) для потока событий IdP.
|
||||||
|
# Скрипт и список событий — через configMapGenerator: хеш в имени ConfigMap меняет spec Job'а,
|
||||||
|
# и Flux (kustomize.toolkit.fluxcd.io/force) пересоздаёт неизменяемый Job при правке любого из них.
|
||||||
|
# namespace задаёт base (security-audit).
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- serviceaccount.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
|
- job.yaml
|
||||||
|
# ${...} в скрипте — переменные sh: в контурах с postBuild.substitute Flux не должен их подставлять.
|
||||||
|
generatorOptions:
|
||||||
|
annotations:
|
||||||
|
kustomize.toolkit.fluxcd.io/substitute: disabled
|
||||||
|
configMapGenerator:
|
||||||
|
- name: idp-actions-bootstrap-script
|
||||||
|
files:
|
||||||
|
- files/idp-actions-bootstrap.sh
|
||||||
|
- name: idp-actions-bootstrap-events
|
||||||
|
files:
|
||||||
|
- files/events.txt
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
# Дополняет общую audit-collector (podSelector: {} — действует и на под Job'а: DNS, Vault, Kafka).
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: idp-actions-bootstrap
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
security-audit/job: idp-actions-bootstrap
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
ingress:
|
||||||
|
- ports:
|
||||||
|
- {port: 15021, protocol: TCP} # readiness istio-proxy (kubelet)
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels: {kubernetes.io/metadata.name: zitadel}
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: idp-contour
|
||||||
|
app.kubernetes.io/component: start
|
||||||
|
ports:
|
||||||
|
- {port: 8080, protocol: TCP}
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels: {kubernetes.io/metadata.name: istio-system}
|
||||||
|
podSelector:
|
||||||
|
matchLabels: {app: istiod}
|
||||||
|
ports:
|
||||||
|
- {port: 15012, protocol: TCP} # xDS и сертификаты istio-proxy
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
# = bound SA роли Vault security-audit-bootstrap. Доступ к Kubernetes API не нужен.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: idp-actions-bootstrap
|
||||||
@ -5,5 +5,7 @@ resources:
|
|||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- networkpolicy.yaml
|
- networkpolicy.yaml
|
||||||
- networkpolicy-access-collector.yaml
|
- networkpolicy-access-collector.yaml
|
||||||
|
- authorizationpolicy-access-collector.yaml
|
||||||
- helmrelease-collector.yaml
|
- helmrelease-collector.yaml
|
||||||
- helmrelease-access-collector.yaml
|
- helmrelease-access-collector.yaml
|
||||||
|
- idp-actions-bootstrap
|
||||||
|
|||||||
@ -22,6 +22,17 @@ spec:
|
|||||||
- ports:
|
- ports:
|
||||||
- {port: 15020, protocol: TCP}
|
- {port: 15020, protocol: TCP}
|
||||||
- {port: 15021, protocol: TCP}
|
- {port: 15021, protocol: TCP}
|
||||||
|
# События Zitadel (webhookevent) — только от подов Zitadel. Метки — чарт idp-contour, одинаковы во всех контурах.
|
||||||
|
# Правило добавлено последним: патч оверлея ссылается на /spec/ingress/0.
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels: {kubernetes.io/metadata.name: zitadel}
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: idp-contour
|
||||||
|
app.kubernetes.io/component: start
|
||||||
|
ports:
|
||||||
|
- {port: 8088, protocol: TCP}
|
||||||
egress:
|
egress:
|
||||||
# xDS и сертификаты istio-proxy от istiod.
|
# xDS и сертификаты istio-proxy от istiod.
|
||||||
- to:
|
- to:
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Zitadel контура: infrastructure/zitadel/uralkal (ExternalDomain, ExternalSecure: true), Service чарта idp-contour.
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: idp-actions-bootstrap
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: bootstrap
|
||||||
|
env:
|
||||||
|
- name: ZITADEL_API
|
||||||
|
value: http://zitadel-idp-contour.zitadel.svc.cluster.local:8080
|
||||||
|
# Инстанс по x-zitadel-instance-host. TODO(verify): запрос с этим заголовком к Service проходит
|
||||||
|
# (см. README, «События Zitadel» → проверка API изнутри кластера).
|
||||||
|
- name: ZITADEL_HOST
|
||||||
|
value: login.sarex.local.uralkali.com
|
||||||
@ -6,6 +6,7 @@ resources:
|
|||||||
patches:
|
patches:
|
||||||
- path: audit-collector.yaml
|
- path: audit-collector.yaml
|
||||||
- path: access-collector.yaml
|
- path: access-collector.yaml
|
||||||
|
- path: idp-actions-bootstrap.yaml
|
||||||
# Селектор ingress-шлюза (HelmRelease istio-system/ingressgateway): replace, а не merge —
|
# Селектор ingress-шлюза (HelmRelease istio-system/ingressgateway): replace, а не merge —
|
||||||
# merge-patch CRD оставил бы и заглушку security-audit/gateway.
|
# merge-patch CRD оставил бы и заглушку security-audit/gateway.
|
||||||
- target:
|
- target:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user