# 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`): ```bash kubectl -n kafka exec -it kafka-kafka-contour-controller-0 -c kafka -- bash ``` ```bash cat > /tmp/admin.properties <]' 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: # блокер: 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`. Записи сверх этого теряются, шлюз не блокируется. ## Проверить перед подключением Сверено по репозиторию и чартам 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.` (контекст выводится автоматически).