iac/infrastructure/security-audit
2026-11-02 00:33:20 +03:00
..
base **Add HelmRelease access-collector for processing ingress-gateway access logs in security-audit namespace with Kafka and Vault integration** 2026-11-02 00:33:20 +03:00
mesh **Add HelmRelease access-collector for processing ingress-gateway access logs in security-audit namespace with Kafka and Vault integration** 2026-11-02 00:33:20 +03:00
uralkal **Add HelmRelease access-collector for processing ingress-gateway access logs in security-audit namespace with Kafka and Vault integration** 2026-11-02 00:33:20 +03:00
kustomization.yaml Add audit collector HelmRelease for security-audit namespace to process and export kube-apiserver audit logs, including Kafka and Vault integration 2026-11-01 22:02:54 +03:00
README.md **Add HelmRelease access-collector for processing ingress-gateway access logs in security-audit namespace with Kafka and Vault integration** 2026-11-02 00:33:20 +03:00

security-audit

Поток ИБ в Kafka, два источника:

Источник Коллектор Топик
журнал аудита kube-apiserver (мастера) DaemonSet audit-collector security.k8s-audit
журнал доступа ingress-шлюза Istio Deployment access-collector security.mesh-access

Одно сообщение — одна запись в JSON (encoding: raw, без OTLP-обёртки), обогащённая полями sarex_*. Дальнейшие этапы (processor → MinIO/Parquet) — отдельными компонентами.

С observability-коллектором (infrastructure/opentelemetry-collector) не связан: отдельный релиз, namespace и конфиг.

Каталог Что Namespace
base/ оба коллектора, NetworkPolicy, Namespace 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 вернётся.

Предусловия подключения в контуре

  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):

    kubectl -n kafka exec -it kafka-kafka-contour-controller-0 -c kafka -- 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:

  - ../../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 с владельцами приложений):

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 даст предупреждения при создании подов — ожидаемо.

Проверка после подключения

  • Провайдер и порядок фильтров на шлюзе:
    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. Записи сверх этого теряются, шлюз не блокируется.

Проверить перед подключением

Сверено по репозиторию и чартам 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) схемой не проверяется

Остаётся открытым:

  • 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").
  • Некритичные предупреждения 0.121.0: service::telemetry::metrics::address устарел (так же во всех коллекторах репо); OTTL-пути без префикса log. (контекст выводится автоматически).