44 KiB
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 |
Одно сообщение — одна запись в 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 |
— |
| Оверлей | Статус |
|---|---|
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 вернётся.
Предусловия подключения в контуре
-
Аудит 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есть на всех мастерах. -
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):kubectl -n kafka exec -it kafka-kafka-contour-controller-0 -c kafka -- bashcat > /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 - Топик
-
Vault (
secrets/— KV v2):- секрет
secrets/data/security-audit/kafkaс ключамиusername,password,ca_crt;ca_crt— содержимоеca.crtиз Secretkafka/kafka-kafka-contour-tls(CA генерирует чартkafka-contour, срок 365 дней; при перевыпуске обновить и здесь); - политика
readнаsecrets/data/security-audit/kafka; - роль
security-auditвauth/kubernetes: bound SAaudit-collectorиaccess-collector, namespacesecurity-audit, политика выше.
Заводится в
infra/terraform, веткаcontour, модульvault-platform-secrets(ролиvault_kubernetes_auth_backend_role, KV-секреты). - секрет
-
Образ
cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib:0.121.0-amd64доступен в registry контура (тот же, что дефолт чартаopentelemetry-collector-prod0.117.1),regcredесть в namespacesecurity-audit.
Подключение
Отдельным MR после выполнения предусловий — добавить в clusters/uralkal/kustomization.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.PrivateKeyLifetime6h), 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, endpointzitadel-idp-contour.zitadel.svc.cluster.local). Ключи забирает Envoy шлюза через кластер mesh, без внешнего DNS и CA.Hostостаётся внешним доменом инстанса, его Zitadel узнаёт. Прямойhttp://zitadel-idp-contour.zitadel.svc:8080/...Zitadel отклонит: инстанс определяется поHost.
Черновик (не подключён; включать после закрытия таблицы и согласования 401 с владельцами приложений):
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(webhookobject.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: enforceprivilegedэто разрешает, профильwarn: baselineдаст предупреждения при создании подов — ожидаемо.
Проверка после подключения
- Провайдер и порядок фильтров на шлюзе:
Первым в списке —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' | uniqsecurity-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/...→ в Kafkauser_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) копируетjq1.6 с библиотеками в emptyDir. Скрипт идёт вcontour/kafka-nn/kafka:4.0.0-debian-12-r10(curl7.88). Оба на Debian 12.TODO(verify): заменить на один toolbox-образ. -
Инстанс Zitadel выбирается заголовком
x-zitadel-instance-host(+x-zitadel-public-host,X-Forwarded-Proto: https). ЭтоInstanceHostHeaders/PublicHostHeadersизdefaults.yamlv4.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) идемпотентен:- ждёт
/debug/ready(10 попыток × 15 с); - печатает все типы событий инстанса (
known: …,POST /admin/v1/events/types/_search); - ищет Target
security-audit-idp-eventsпо имени и создаёт или обновляет его (restAsync, endpoint,timeout: 10s); - для каждого события —
PUT /v2/actions/executionsс условием{"event": {"event": "<тип>"}}. Чужие targets Execution'а сохраняются, свой добавляется в конец; - неизвестное имя события — ошибка: остальные события обрабатываются, код выхода ≠ 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 — во Vaultsecrets/data/security-audit/zitadel-bootstrap, ключpat. - Роль Vault
security-audit-bootstrapвauth/kubernetes: bound SAidp-actions-bootstrap, namespacesecurity-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"):
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, но не от SAzitadel→403 RBAC: access denied.
Риски
-
Повторов доставки нет. Для
restAsyncZitadel 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). Суточную сверку количества по типам можно добавить позже. -
Размер тела.
webhookevent0.121 читает тело однимbufio.Scannerбез явного буфера: событие больше ~64 КиБ, вероятно, отбрасывается. Ответ при этом 200.TODO(verify)на стенде; события пользователей обычно в разы меньше. -
В 0.121 у
webhookeventнетheader_attribute_regex, есть толькоrequired_header: заголовки в запись не попадают.
Откат
Снять Target со всех Execution'ов из списка и удалить его — скрипт с --remove:
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, доступа к кластеру не было.
| Что | Результат | Как проверено |
|---|---|---|
Образ и 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 контура |
| Скрипт | 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,
issuerZitadel (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.- Некритичные предупреждения 0.121.0:
service::telemetry::metrics::addressустарел (так же во всех коллекторах репо); OTTL-пути без префиксаlog.(контекст выводится автоматически).