691 lines
62 KiB
Markdown
691 lines
62 KiB
Markdown
# security-audit
|
||
|
||
Поток ИБ в 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) — отдельными компонентами.
|
||
|
||
С observability-коллектором (`infrastructure/opentelemetry-collector`) не связан: отдельный релиз, namespace и конфиг.
|
||
|
||
| Каталог | Что | Namespace |
|
||
|---|---|---|
|
||
| `base/` | оба коллектора, NetworkPolicy, AuthorizationPolicy, Namespace | `security-audit` |
|
||
| `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` |
|
||
|
||
| Оверлей | Статус |
|
||
|---|---|
|
||
| `uralkal` | манифесты готовы, **в `clusters/uralkal` не подключён** |
|
||
|
||
## Журнал аудита kube-apiserver
|
||
|
||
DaemonSet `audit-collector-agent` на узлах `node-role.kubernetes.io/control-plane`. Он читает
|
||
`/var/log/audit/kube-apiserver-audit*.log` (текущий и ротированные файлы), позиции хранит в `/var/lib/otelcol-audit`
|
||
(hostPath), туда же складывает дисковую очередь отправки.
|
||
|
||
Обработка (`transform/audit`):
|
||
|
||
- **вырезание шума:** `metadata.managedFields` и аннотация `kubectl.kubernetes.io/last-applied-configuration`
|
||
в `requestObject` / `responseObject`;
|
||
- **маскирование:** значения параметров `password|passwd|pwd|token|secret|api_key|api-key` в `requestURI`
|
||
(`token=abc` → `token=***`), сама команда `exec` не маскируется;
|
||
- **классификация** — поля ниже. Правила применяются по порядку, последнее подходящее побеждает.
|
||
|
||
| Поле | Значение | Условие |
|
||
|---|---|---|
|
||
| `sarex_contour` | имя контура | env `CONTOUR` из оверлея |
|
||
| `sarex_dry_run` | `true` / `false` | `requestURI` содержит `dryRun=All` |
|
||
| `sarex_action` | `read` | `verb` ∈ `get`, `list` |
|
||
| | `mutate` | `verb` ∈ `create`, `update`, `patch`, `delete`, `deletecollection` |
|
||
| | `status` | `objectRef.subresource == status` |
|
||
| | `interactive` | `objectRef.subresource` ∈ `exec`, `attach`, `portforward` |
|
||
| `sarex_outcome` | `applied` | код 2xx, не dry-run |
|
||
| | `dry_run` | код 2xx, dry-run |
|
||
| | `denied_authn` | 401 |
|
||
| | `denied_authz` | 403 и `authorization.k8s.io/decision=forbid` |
|
||
| | `denied_admission` | 4xx при `decision=allow`, кроме 404/409/422 |
|
||
| | `invalid` | 422 |
|
||
| | `conflict` | 409 |
|
||
| | `not_found` | 404 |
|
||
| | `error` | 5xx |
|
||
| `sarex_actor_type` | `human` | по умолчанию |
|
||
| | `serviceaccount` | `system:serviceaccount:*` |
|
||
| | `controller` | `system:serviceaccount:kube-system:*`, `system:kube-*`, `system:apiserver` |
|
||
| | `node` | `system:node:*` |
|
||
| | `gitops` | `system:serviceaccount:flux-system:*` |
|
||
|
||
`watch` не попадает ни в одну категорию `sarex_action`: поле не выставляется.
|
||
|
||
Надёжность доставки: `required_acks: -1`, дисковая очередь (`sending_queue.storage`), бесконечные ретраи. При
|
||
заполненной очереди срабатывает backpressure на `filelog`, чтение файла приостанавливается. Данные теряются,
|
||
только если файл успеет уйти из ротации (10 × 100 МБ) раньше, чем Kafka вернётся.
|
||
|
||
## Предусловия подключения в контуре
|
||
|
||
1. **Аудит apiserver включён** (INFRA-3057): флаги `--audit-policy-file`, `--audit-log-path=/var/log/audit/kube-apiserver-audit.log`,
|
||
`--audit-log-maxsize=100`, `--audit-log-maxbackup=10`. Каталог `/var/log/audit` есть на всех мастерах.
|
||
2. **Kafka:** топик и SCRAM-пользователь.
|
||
- Топик `security.k8s-audit`: 1 партиция, `retention.ms=259200000` (3 суток), `max.message.bytes=5242880`,
|
||
RF по контуру (в `uralkal` один controller → RF 1).
|
||
- Пользователь `security-audit` (SCRAM-SHA-512). ACL не нужны.
|
||
|
||
Заводится terraform'ом: `infra/terraform`, ветка `contour`, модуль `kafka-topics-incluster`, блок
|
||
`environments.<контур>` в `infrastructure.yaml`. Если нужно вручную — из пода контроллера, через внутренний
|
||
листенер (`INTERNAL`, 9094, `SASL_PLAINTEXT`/`PLAIN`, `inter_broker_user`):
|
||
|
||
```bash
|
||
kubectl -n kafka exec -it kafka-kafka-contour-controller-0 -c kafka -- bash
|
||
```
|
||
|
||
```bash
|
||
cat > /tmp/admin.properties <<EOF
|
||
security.protocol=SASL_PLAINTEXT
|
||
sasl.mechanism=PLAIN
|
||
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="inter_broker_user" password="$(cat /vault/secrets/kafka-inter-broker-password)";
|
||
EOF
|
||
B=localhost:9094
|
||
|
||
kafka-topics.sh --bootstrap-server $B --command-config /tmp/admin.properties --create \
|
||
--topic security.k8s-audit --partitions 1 --replication-factor 1 \
|
||
--config retention.ms=259200000 --config max.message.bytes=5242880
|
||
|
||
kafka-configs.sh --bootstrap-server $B --command-config /tmp/admin.properties --alter \
|
||
--entity-type users --entity-name security-audit \
|
||
--add-config 'SCRAM-SHA-512=[iterations=4096,password=<пароль>]'
|
||
|
||
rm /tmp/admin.properties
|
||
```
|
||
3. **Vault** (`secrets/` — KV v2):
|
||
- секрет `secrets/data/security-audit/kafka` с ключами `username`, `password`, `ca_crt`;
|
||
`ca_crt` — содержимое `ca.crt` из Secret `kafka/kafka-kafka-contour-tls` (CA генерирует чарт `kafka-contour`,
|
||
срок 365 дней; при перевыпуске обновить и здесь);
|
||
- политика `read` на `secrets/data/security-audit/kafka`;
|
||
- роль `security-audit` в `auth/kubernetes`: bound SA `audit-collector` **и** `access-collector`, namespace
|
||
`security-audit`, политика выше.
|
||
|
||
Заводится в `infra/terraform`, ветка `contour`, модуль `vault-platform-secrets` (роли `vault_kubernetes_auth_backend_role`,
|
||
KV-секреты).
|
||
4. **Образ** `cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64`
|
||
доступен в registry контура (тот же, что дефолт чарта `opentelemetry-collector-prod` 0.117.1), `regcred` есть в
|
||
namespace `security-audit`.
|
||
|
||
## Подключение
|
||
|
||
Отдельным MR после выполнения предусловий — добавить в `clusters/uralkal/kustomization.yaml`:
|
||
|
||
```yaml
|
||
- ../../infrastructure/security-audit/uralkal
|
||
```
|
||
|
||
`dependsOn` в оверлее ждёт HelmRelease `kafka/kafka` и `vault/vault`.
|
||
|
||
Порядок: сначала должен раскататься провайдер `audit-access` в istiod (`infrastructure/istio-pilot/uralkal/istio-pilot.yaml`,
|
||
применяется сразу, как только попадёт в master: кластер его уже подключает). Telemetry из `mesh/` ссылается на этот провайдер по имени. Проверка:
|
||
`kubectl -n istio-system get cm istio -o jsonpath='{.data.mesh}' | grep -A2 audit-access`.
|
||
|
||
## Проверка после подключения
|
||
|
||
- DaemonSet `security-audit/audit-collector-agent` только на мастерах, все поды `Ready`:
|
||
`kubectl -n security-audit get pods -o wide`.
|
||
- В логах коллектора нет ошибок: `kubectl -n security-audit logs ds/audit-collector-agent -c opentelemetry-collector-prod`.
|
||
- События в топике (из пода контроллера Kafka, с `admin.properties` выше):
|
||
`kafka-console-consumer.sh --bootstrap-server localhost:9094 --consumer.config /tmp/admin.properties --topic security.k8s-audit --max-messages 5`.
|
||
- `kubectl create secret generic audit-probe --from-literal=k=v` → событие с `objectRef.resource=secrets` уровня
|
||
`Metadata`, без `requestObject`/`responseObject`.
|
||
- Рестарт пода коллектора (`kubectl delete pod`) → нет дублей и пропусков: сверить `auditID` в топике с файлом журнала
|
||
на мастере за интервал рестарта.
|
||
|
||
## Известное ограничение
|
||
|
||
Значения env с паролями внутри `requestObject` / `responseObject` workload-ресурсов на уровне `RequestResponse`
|
||
коллектор не маскирует: OTTL плохо работает с массивами (`spec.containers[].env[]`). Маскирование будет в processor'е.
|
||
До его готовности не повышать уровень для workload-ресурсов выше `Request` в контурах, где пароли могут быть в env.
|
||
|
||
## Журнал доступа шлюза
|
||
|
||
Каждый внешний HTTP-запрос через ingress-шлюз (`istio-system`, `istio: ingressgateway`): кто, что, когда, результат,
|
||
IP, `x-request-id`, `trace_id`. Поток ИБ отдельно от прикладных журналов (п.20); IP источника для событий приложений
|
||
находится по `request_id` (п.15).
|
||
|
||
```
|
||
ingress-шлюз (Envoy)
|
||
└─ Telemetry gateway-audit-access ─► провайдер audit-access (envoyOtelAls, meshConfig istiod)
|
||
└─ OTLP gRPC, mTLS ─► access-collector.security-audit:4317 (2 реплики, sidecar Istio)
|
||
└─ transform/access ─► Kafka security.mesh-access ─► processor (infra/k8s-audit, отдельная задача)
|
||
```
|
||
|
||
### Поля записи
|
||
|
||
| Поле | Источник Envoy | Примечание |
|
||
|---|---|---|
|
||
| `sarex_kind` | — | всегда `mesh_access` |
|
||
| `start_time` | `%START_TIME%` | |
|
||
| `request_id` | `%REQ(X-REQUEST-ID)%` | ключ связи с журналами приложений (п.15) |
|
||
| `trace_id` | `%TRACE_ID%` | пусто, пока в mesh выключен трейсинг |
|
||
| `authority`, `method`, `protocol` | `:authority`, `:method`, `%PROTOCOL%` | |
|
||
| `path` | `%REQ_WITHOUT_QUERY(:PATH)%` | без query; маскирование — ниже |
|
||
| `response_code`, `response_flags` | `%RESPONSE_CODE%`, `%RESPONSE_FLAGS%` | `0` — ответа не было |
|
||
| `duration_ms`, `bytes_in`, `bytes_out` | `%DURATION%`, `%BYTES_RECEIVED%`, `%BYTES_SENT%` | |
|
||
| `x_forwarded_for`, `downstream_ip` | `X-Forwarded-For`, `%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%` | за NodePort — см. открытые вопросы |
|
||
| `user_sub`, `user_org` | `x-sarex-audit-sub`, `x-sarex-audit-org` | заполняет RequestAuthentication из JWT; **пока её нет — пусто** |
|
||
| `user_agent`, `route`, `upstream_cluster` | `User-Agent`, `%ROUTE_NAME%`, `%UPSTREAM_CLUSTER%` | |
|
||
| `sarex_contour` | env `CONTOUR` | |
|
||
| `sarex_action` | по `method` | `read`: GET/HEAD/OPTIONS; `mutate`: POST/PUT/PATCH/DELETE |
|
||
| `sarex_outcome` | по `response_code` | `applied` 2xx–3xx, `denied_authn` 401, `denied_authz` 403, `invalid` прочие 4xx (включая 404), `error` 5xx и `0` |
|
||
| `sarex_actor_type` | по `user_sub` | `human`, если есть, иначе `anonymous` |
|
||
|
||
**Не логируется (п.23):**
|
||
- `Authorization`, `Cookie`, `Set-Cookie` — это сами учётные данные: журнал с ними позволяет войти от имени пользователя;
|
||
- query-строка — в ней бывают токены публичных ссылок, `token=`, подписи S3 (`X-Amz-Signature`).
|
||
|
||
Маскирование в пути:
|
||
- сегмент после `token|secret|key|signature/` → `***`;
|
||
- JWT-подобные строки → `***jwt***`.
|
||
|
||
Защита от подмены: EnvoyFilter `strip-audit-headers` первым HTTP-фильтром удаляет входящие `x-sarex-audit-sub/org`.
|
||
Без него клиент мог бы записать в журнал любого пользователя.
|
||
|
||
### Пользователь в журнале: RequestAuthentication — не создана (блокер)
|
||
|
||
`user_sub`/`user_org` должна заполнять `RequestAuthentication` на шлюзе (`outputClaimToHeaders`). Но она отклоняет
|
||
**любой** запрос с JWT издателя, которого нет в `jwtRules` (`Jwt issuer is not configured`, 401), на всех хостах шлюза.
|
||
А издатели в Уралкалии подтверждаются по репозиторию не все:
|
||
|
||
| Издатель | Заголовок | `issuer` / JWKS | Статус |
|
||
|---|---|---|---|
|
||
| Zitadel | `Authorization: Bearer`, `Identity` | `https://login.sarex.local.uralkali.com` (`ExternalDomain` + `ExternalSecure: true` в `infrastructure/zitadel/uralkal`; `ExternalPort` не задан — проверить, что в `/.well-known/openid-configuration` нет `:8080`) | issuer — `TODO(verify)`; префикс заголовка `Identity` не подтверждён |
|
||
| django (SimpleJWT, RS512) | `Authorization: Bearer` (режим `sarex`) | в коде `SIMPLE_JWT_ISSUER` по умолчанию `default_issuer` (`apps/django/base/django-configmap.yaml`), в `CONFIGURATION.md` — `django`; в `uralkal` не задан. Ключ — `secrets/data/vault/common/rsa_keys`, в репозитории нет | **не подтверждён** |
|
||
| Camunda Keycloak | `Authorization: Bearer` на `camunda-*` | `https://camunda-keycloak.sarex.local.uralkali.com/auth/realms/camunda-platform` (`infrastructure/camunda/uralkal`), JWKS `…/protocol/openid-connect/certs` | подтверждён |
|
||
| document-link | `Authorization: Bearer` (зашитый/сервисный JWT) | неизвестен | **не подтверждён** |
|
||
|
||
Пока таблица не закрыта, журнал пишется без пользователя (`sarex_actor_type=anonymous`). Пользователя для события
|
||
можно найти по `request_id` в журналах приложений.
|
||
|
||
JWKS Zitadel — выбранный вариант, когда дойдёт до включения:
|
||
- **inline `jwks` — нет:** Zitadel ротирует ключи подписи по умолчанию раз в часы (`KeyConfig.PrivateKeyLifetime` 6h),
|
||
inline-набор устареет за день, и шлюз начнёт отдавать 401 всем.
|
||
- **`jwksUri` на внешний адрес, загрузка istiod — только если подтверждено:** istiod резолвит
|
||
`login.sarex.local.uralkali.com` и доверяет CA сертификата `uralkal-sarex-tls`. В istiod нет CA заказчика, и
|
||
отключать проверку (`JWKS_RESOLVER_INSECURE_SKIP_VERIFY`) нельзя.
|
||
- **Рекомендуется:** `PILOT_JWT_ENABLE_REMOTE_JWKS=envoy` (env istiod) и `jwksUri: http://login.sarex.local.uralkali.com:8080/oauth/v2/keys`
|
||
плюс `ServiceEntry` хоста `login.sarex.local.uralkali.com:8080` (HTTP, `resolution: DNS`, endpoint
|
||
`zitadel-idp-contour.zitadel.svc.cluster.local`). Ключи забирает Envoy шлюза через кластер mesh, без внешнего DNS и CA.
|
||
`Host` остаётся внешним доменом инстанса, его Zitadel узнаёт. Прямой `http://zitadel-idp-contour.zitadel.svc:8080/...`
|
||
Zitadel отклонит: инстанс определяется по `Host`.
|
||
|
||
Черновик (не подключён; включать после закрытия таблицы и согласования 401 с владельцами приложений):
|
||
|
||
```yaml
|
||
apiVersion: security.istio.io/v1
|
||
kind: RequestAuthentication
|
||
metadata: {name: audit-jwt, namespace: istio-system}
|
||
spec:
|
||
selector: {matchLabels: {istio: ingressgateway}}
|
||
jwtRules:
|
||
- issuer: https://login.sarex.local.uralkali.com # TODO(verify)
|
||
jwksUri: http://login.sarex.local.uralkali.com:8080/oauth/v2/keys
|
||
forwardOriginalToken: true
|
||
fromHeaders:
|
||
- {name: Authorization, prefix: "Bearer "}
|
||
- {name: Identity, prefix: "Bearer "} # TODO(verify): префикс
|
||
outputClaimToHeaders:
|
||
- {header: x-sarex-audit-sub, claim: sub}
|
||
- {header: x-sarex-audit-org, claim: "urn:zitadel:iam:user:resourceowner:id"} # TODO(verify): есть ли claim в access token
|
||
- issuer: https://camunda-keycloak.sarex.local.uralkali.com/auth/realms/camunda-platform
|
||
jwksUri: http://camunda-keycloak.camunda.svc.cluster.local/auth/realms/camunda-platform/protocol/openid-connect/certs # TODO(verify)
|
||
forwardOriginalToken: true
|
||
# - issuer: <django SIMPLE_JWT_ISSUER> # блокер: issuer и публичный ключ
|
||
```
|
||
|
||
### Предусловия подключения
|
||
|
||
- Топик `security.mesh-access`: 3 партиции, `retention.ms=259200000`, `max.message.bytes=1048576`, RF по контуру —
|
||
тем же путём, что `security.k8s-audit` (terraform или команды выше). Пользователь — тот же `security-audit`, ACL не нужны.
|
||
- Роль Vault `security-audit`: в bound SA добавлен `access-collector` (см. предусловия выше).
|
||
- Провайдер `audit-access` раскатан в istiod (см. «Подключение»).
|
||
|
||
### Sidecar и Vault
|
||
|
||
- Namespace `security-audit` без `istio-injection`: sidecar получают только поды с меткой `sidecar.istio.io/inject=true`
|
||
(webhook `object.sidecar-injector.istio.io`). Это `access-collector`. У `audit-collector` метка `"false"`.
|
||
Метка `istio-injection: disabled` отключила бы инъекцию совсем, даже по метке пода.
|
||
- `PeerAuthentication` в репозитории нет, поэтому режим PERMISSIVE. Шлюз шлёт в коллектор по auto-mTLS (оба в mesh),
|
||
коллектор в Kafka — тоже через mTLS (`kafka` в mesh), внутри остаётся `SASL_SSL`.
|
||
- Vault Agent (`agent-init-first`, pre-populate) + istio-proxy: по паттерну контура (`apps/bi`, `apps/bim`, `apps/cde`…)
|
||
`traffic.sidecar.istio.io/excludeOutboundPorts: "8200"`. Init-агент ходит в Vault мимо iptables-перехвата,
|
||
даже если `istio-init` отработал раньше.
|
||
- `holdApplicationUntilProxyStarts`: kafka exporter подключается к брокерам на старте и без готового proxy падает.
|
||
- `istio-init` требует `NET_ADMIN`/`NET_RAW`: enforce `privileged` это разрешает, профиль `warn: baseline` даст
|
||
предупреждения при создании подов — ожидаемо.
|
||
|
||
### Проверка после подключения
|
||
|
||
- Провайдер и порядок фильтров на шлюзе:
|
||
```bash
|
||
POD=$(kubectl -n istio-system get pod -l istio=ingressgateway -o jsonpath='{.items[0].metadata.name}')
|
||
istioctl -n istio-system proxy-config cluster $POD | grep access-collector
|
||
istioctl -n istio-system proxy-config listener $POD -o json \
|
||
| jq -r '.[].filterChains[].filters[] | select(.name=="envoy.filters.network.http_connection_manager")
|
||
| .typedConfig.httpFilters[].name' | uniq
|
||
```
|
||
Первым в списке — `security-audit.strip-audit-headers`, раньше `envoy.filters.http.jwt_authn` (когда появится).
|
||
В выводе `listener` есть `envoy.access_loggers.open_telemetry`.
|
||
- `curl -H 'x-sarex-audit-sub: spoof' https://sarex.local.uralkali.com/...` → в Kafka `user_sub` пустой.
|
||
- Запрос с `?token=canary` → строки `canary` нет в топике.
|
||
- После включения RequestAuthentication:
|
||
- запрос с валидным токеном → `user_sub` заполнен;
|
||
- с невалидным токеном Zitadel → 401 (так работает RequestAuthentication). **Согласовать с владельцами приложений до включения.**
|
||
- `kubectl -n security-audit scale deploy/access-collector --replicas=0` → трафик шлюза не падает (ALS асинхронный).
|
||
Потери видны в статистике Envoy: `kubectl -n istio-system exec $POD -- pilot-agent request GET stats | grep access_logs`.
|
||
Вернуть 2 реплики.
|
||
- Чтение топика: как для `security.k8s-audit`, `--topic security.mesh-access`.
|
||
|
||
### Мониторинг
|
||
|
||
- `otelcol_receiver_accepted_log_records{receiver="otlp"}` у `access-collector`: алерт, если 0 за 10 минут при живом
|
||
трафике шлюза (`istio_requests_total` на `ingressgateway`).
|
||
- `otelcol_exporter_sent_log_records` / `otelcol_exporter_send_failed_log_records` (`exporter="kafka/access"`),
|
||
`otelcol_exporter_queue_size` против `otelcol_exporter_queue_capacity`, `otelcol_exporter_enqueue_failed_log_records`.
|
||
- Метрики коллектора отдаются на `:8888` пода. Prometheus в Уралкалии по репозиторию не найден — сбор метрик отдельно.
|
||
|
||
### Открытые вопросы
|
||
|
||
- **IP клиента (п.15).** Шлюз — одна реплика на control-plane за Service `NodePort` (30080/30443) без
|
||
`externalTrafficPolicy`, т. е. `Cluster`. kube-proxy делает SNAT, и в `downstream_ip` попадает IP узла, а не клиента.
|
||
Нужно выяснить, есть ли перед NodePort балансировщик заказчика и передаёт ли он `X-Forwarded-For`. Если да —
|
||
`meshConfig.defaultConfig.gatewayTopology.numTrustedProxies` (или аннотация `proxy.istio.io/config` на шлюзе) по числу
|
||
прокси, иначе Envoy не доверяет XFF. Если балансировщика нет — `externalTrafficPolicy: Local` и DNS/LB только на
|
||
узел шлюза. Ничего из этого не меняли.
|
||
- **Статика** (`*/static/*`, фронтенды) пишется в журнал. Исключать ли — решение ИБ (фильтр в `mesh/telemetry.yaml`).
|
||
- **Запросы между сервисами** (sidecar'ы приложений) — отдельный Telemetry с фильтром, не в этой задаче.
|
||
- **Очередь в памяти:** `queue_size: 50000` считается в запросах (батчах до 2000 записей), а не в записях. При долгой
|
||
недоступности 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` или модуль из кластера.
|
||
|
||
## Журнал аудита 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, доступа к кластеру не было.
|
||
|
||
| Что | Результат | Как проверено |
|
||
|---|---|---|
|
||
| Образ и `command.name` | `.../opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64`, `otelcol-contrib` | дефолт `values.yaml` чарта `opentelemetry-collector-prod` 0.117.1 (appVersion 0.120.0, тег образа 0.121.0); `otelcol-contrib components` в образе: filelog, transform, kafka, file_storage, health_check, memory_limiter, batch есть |
|
||
| Синтаксис kafka exporter | для 0.121.0 корректен (`topic`/`encoding` наверху, `auth.tls`) | `otelcol-contrib validate` в том же образе — OK |
|
||
| Kafka Service и порт | `kafka-kafka-contour.kafka.svc.cluster.local:9092`, `SASL_SSL` (**не** `kafka.kafka...`) | `helm template` чарта `kafka-contour` 0.1.15 с values `uralkal`; тот же адрес у `apps/*/uralkal` |
|
||
| CA Kafka | Secret `kafka/kafka-kafka-contour-tls`, ключ `ca.crt` (helm `genCA`, 365 дней) | шаблон `tls-secret.yaml` чарта; `apps/issues/uralkal` монтирует его же |
|
||
| Kafka 4.0.0 и `protocol_version: 2.8.0` | совместимо (Kafka 4 требует ≥ 2.1) | `appVersion` чарта `kafka-contour` |
|
||
| Vault | HelmRelease `vault/vault`; injector ходит на `http://vault-vault-contour.vault.svc:8200` | `infrastructure/vault/uralkal`, `helm template` `vault-contour` 0.2.3 |
|
||
| Узлы мастеров | метка и taint `node-role.kubernetes.io/control-plane` | так же сажается `istio-gateway/uralkal` |
|
||
| Рендер чарта | нет `hostPort`, Service, ClusterRole; nodeSelector/tolerations/securityContext как заданы | `helm template` 0.117.1 с values из собранного HelmRelease |
|
||
| Логика `transform` | 5 тестовых событий классифицированы верно, `token=***`, `managedFields` вырезаны | запуск коллектора локально в том же образе с `file`-экспортёром вместо kafka |
|
||
| **Журнал доступа шлюза** (02.11.2026) | | |
|
||
| Шлюз | HelmRelease `istio-system/ingressgateway`, метка `istio: ingressgateway`, 1 реплика на control-plane, `NodePort` 30080/30443, `externalTrafficPolicy` не задан (`Cluster`) | `helm template` `istio-gateway-contour` 1.24.0 с values `uralkal` |
|
||
| istiod | `istiod-contour` 1.24.0, ревизий нет; кроме `extensionProviders` mesh config не изменился | `helm template` до и после правки, diff только в ConfigMap `istio` |
|
||
| Инъекция sidecar | ns `istio-injection=enabled` или ns без метки + метка пода `sidecar.istio.io/inject=true`; `disabled` блокирует оба | селекторы MutatingWebhookConfiguration из рендера `istiod-contour` |
|
||
| mTLS | `PeerAuthentication` в репозитории нет → PERMISSIVE | поиск по `infrastructure/`, `clusters/uralkal`, чарту `istio-config-contour` |
|
||
| Vault + sidecar | `excludeOutboundPorts: "8200"` рядом с `agent-init-first` | `apps/bi`, `apps/bim`, `apps/cde/base` |
|
||
| Образ access-collector | тот же; `otlp` receiver, `transform`, `kafka` exporter есть | `otelcol-contrib validate` — OK |
|
||
| Логика `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`) схемой не проверяется |
|
||
| **События 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 контура |
|
||
| **Журнал аудита 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 на машине нет |
|
||
|
||
Остаётся открытым:
|
||
|
||
- `TODO(verify)` **nodelocaldns** (`169.254.25.10`) в NetworkPolicy: в репозитории не подтверждается. При дефолте kubespray
|
||
включён, если нет — правило безвредно.
|
||
- **RequestAuthentication** не создана: не подтверждены издатели django и document-link, `issuer` Zitadel (`TODO(verify)`) —
|
||
см. «Пользователь в журнале». Пока `user_sub` пустой.
|
||
- **IP клиента за NodePort** — см. «Открытые вопросы» журнала доступа.
|
||
- `set(body, attributes)` + `encoding: raw` даёт JSON-объект (проверено локально через `file`-экспортёр, raw-маршалер
|
||
сериализует map в JSON) — подтвердить на стенде чтением топика. Там же проверить, что Envoy шлёт labels строками
|
||
(сравнения `response_code == "401"`).
|
||
- `TODO(verify)` **API Zitadel изнутри кластера** с `x-zitadel-instance-host` (команда в «События Zitadel»): по
|
||
исходникам должно работать, запрос не выполнялся.
|
||
- `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.` (контекст выводится автоматически).
|