**Add HelmRelease access-collector for processing ingress-gateway access logs in security-audit namespace with Kafka and Vault integration**
This commit is contained in:
parent
ad7050ca7d
commit
ae1a683ada
@ -13,3 +13,34 @@ spec:
|
||||
global:
|
||||
imagePullSecrets:
|
||||
- regcred
|
||||
# Провайдер журнала доступа для security-audit (access-collector). Сам по себе ничего не включает:
|
||||
# работает только для workload'ов, на которые ссылается Telemetry (infrastructure/security-audit/mesh).
|
||||
meshConfig:
|
||||
extensionProviders:
|
||||
- name: audit-access
|
||||
envoyOtelAls:
|
||||
service: access-collector.security-audit.svc.cluster.local
|
||||
port: 4317
|
||||
logFormat:
|
||||
labels:
|
||||
sarex_kind: "mesh_access"
|
||||
start_time: "%START_TIME%"
|
||||
request_id: "%REQ(X-REQUEST-ID)%"
|
||||
trace_id: "%TRACE_ID%"
|
||||
authority: "%REQ(:AUTHORITY)%"
|
||||
method: "%REQ(:METHOD)%"
|
||||
path: "%REQ_WITHOUT_QUERY(:PATH)%"
|
||||
protocol: "%PROTOCOL%"
|
||||
response_code: "%RESPONSE_CODE%"
|
||||
response_flags: "%RESPONSE_FLAGS%"
|
||||
duration_ms: "%DURATION%"
|
||||
bytes_in: "%BYTES_RECEIVED%"
|
||||
bytes_out: "%BYTES_SENT%"
|
||||
x_forwarded_for: "%REQ(X-FORWARDED-FOR)%"
|
||||
downstream_ip: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
|
||||
user_sub: "%REQ(X-SAREX-AUDIT-SUB)%"
|
||||
user_org: "%REQ(X-SAREX-AUDIT-ORG)%"
|
||||
user_agent: "%REQ(USER-AGENT)%"
|
||||
route: "%ROUTE_NAME%"
|
||||
upstream_cluster: "%UPSTREAM_CLUSTER%"
|
||||
# Authorization, Cookie, Set-Cookie и query-строка не логируются (п.23)
|
||||
|
||||
@ -1,16 +1,28 @@
|
||||
# security-audit
|
||||
|
||||
Сбор журнала аудита kube-apiserver с мастеров в Kafka-топик `security.k8s-audit`.
|
||||
Одно сообщение — одно событие аудита в JSON (`encoding: raw`, без OTLP-обёртки), обогащённое полями `sarex_*`.
|
||||
Поток ИБ в 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`
|
||||
@ -93,7 +105,8 @@ DaemonSet `audit-collector-agent` на узлах `node-role.kubernetes.io/contr
|
||||
`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`, namespace `security-audit`, политика выше.
|
||||
- роль `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-секреты).
|
||||
@ -111,6 +124,10 @@ DaemonSet `audit-collector-agent` на узлах `node-role.kubernetes.io/contr
|
||||
|
||||
`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`:
|
||||
@ -129,6 +146,167 @@ DaemonSet `audit-collector-agent` на узлах `node-role.kubernetes.io/contr
|
||||
коллектор не маскирует: 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`. Записи сверх этого теряются,
|
||||
шлюз не блокируется.
|
||||
|
||||
## Проверить перед подключением
|
||||
|
||||
Сверено по репозиторию и чартам 01.11.2026, доступа к кластеру не было.
|
||||
@ -144,10 +322,25 @@ DaemonSet `audit-collector-agent` на узлах `node-role.kubernetes.io/contr
|
||||
| Узлы мастеров | метка и 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.` (контекст выводится автоматически).
|
||||
|
||||
@ -0,0 +1,193 @@
|
||||
# Журнал доступа ingress-шлюза Istio: envoyOtelAls (провайдер audit-access в meshConfig istiod) → OTLP gRPC → Kafka.
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: access-collector
|
||||
namespace: security-audit
|
||||
spec:
|
||||
interval: 10m
|
||||
timeout: 10m
|
||||
chart:
|
||||
spec:
|
||||
chart: opentelemetry-collector-prod
|
||||
version: "0.117.1"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: yc-oci-charts
|
||||
namespace: flux-system
|
||||
interval: 10m
|
||||
install:
|
||||
remediation:
|
||||
retries: 3
|
||||
upgrade:
|
||||
remediation:
|
||||
retries: 3
|
||||
values:
|
||||
mode: deployment
|
||||
replicaCount: 2
|
||||
fullnameOverride: access-collector
|
||||
|
||||
# Тот же образ, что у audit-collector: otlp receiver, transform, kafka exporter есть в сборке.
|
||||
image:
|
||||
repository: cr.yandex/crp3ccidau046kdj8g9q/opentelemetry-collector/opentelemetry-collector-contrib
|
||||
tag: "0.121.0-amd64"
|
||||
command:
|
||||
name: otelcol-contrib
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
# Мягкая: при одном доступном узле вторая реплика всё равно встанет.
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
security-audit/collector: access
|
||||
|
||||
clusterRole:
|
||||
create: false
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: access-collector # bound SA роли Vault security-audit (вместе с audit-collector)
|
||||
presets:
|
||||
logsCollection: {enabled: false}
|
||||
hostMetrics: {enabled: false}
|
||||
kubernetesAttributes: {enabled: false}
|
||||
kubeletMetrics: {enabled: false}
|
||||
kubernetesEvents: {enabled: false}
|
||||
clusterMetrics: {enabled: false}
|
||||
|
||||
ports:
|
||||
otlp:
|
||||
enabled: true
|
||||
containerPort: 4317
|
||||
servicePort: 4317
|
||||
protocol: TCP
|
||||
appProtocol: grpc # Istio выбирает протокол по appProtocol
|
||||
otlp-http: {enabled: false}
|
||||
jaeger-compact: {enabled: false}
|
||||
jaeger-thrift: {enabled: false}
|
||||
jaeger-grpc: {enabled: false}
|
||||
zipkin: {enabled: false}
|
||||
metrics: {enabled: false}
|
||||
service:
|
||||
enabled: true
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
seccompProfile: {type: RuntimeDefault}
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
|
||||
resources:
|
||||
requests: {cpu: 100m, memory: 128Mi}
|
||||
limits: {cpu: 1000m, memory: 512Mi}
|
||||
|
||||
podLabels:
|
||||
security-audit/collector: access # селектор NetworkPolicy и anti-affinity
|
||||
sidecar.istio.io/inject: "true" # mTLS внутри mesh (п.30); namespace без istio-injection
|
||||
podAnnotations:
|
||||
sidecar.istio.io/inject: "true"
|
||||
# Kafka exporter подключается к брокерам на старте: без готового istio-proxy коллектор упадёт.
|
||||
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
# Паттерн контура для Vault + sidecar (apps/*/base): трафик агента к Vault мимо istio-proxy.
|
||||
traffic.sidecar.istio.io/excludeOutboundPorts: "8200"
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/agent-init-first: "true"
|
||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||
vault.hashicorp.com/agent-requests-cpu: "25m"
|
||||
vault.hashicorp.com/agent-limits-cpu: "100m"
|
||||
vault.hashicorp.com/auth-path: auth/kubernetes
|
||||
vault.hashicorp.com/role: security-audit
|
||||
# Многострочные шаблоны — см. helmrelease-collector.yaml (toYaml | tpl в чарте).
|
||||
vault.hashicorp.com/agent-inject-secret-kafka-username: secrets/data/security-audit/kafka
|
||||
vault.hashicorp.com/agent-inject-template-kafka-username: |-
|
||||
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
|
||||
{{ "{{ .Data.data.username }}" }}
|
||||
{{ "{{- end -}}" }}
|
||||
vault.hashicorp.com/agent-inject-secret-kafka-password: secrets/data/security-audit/kafka
|
||||
vault.hashicorp.com/agent-inject-template-kafka-password: |-
|
||||
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
|
||||
{{ "{{ .Data.data.password }}" }}
|
||||
{{ "{{- end -}}" }}
|
||||
vault.hashicorp.com/agent-inject-secret-kafka-ca.crt: secrets/data/security-audit/kafka
|
||||
vault.hashicorp.com/agent-inject-template-kafka-ca.crt: |-
|
||||
{{ "{{- with secret \"secrets/data/security-audit/kafka\" -}}" }}
|
||||
{{ "{{ .Data.data.ca_crt }}" }}
|
||||
{{ "{{- end -}}" }}
|
||||
|
||||
# Контур-специфичное (CONTOUR, KAFKA_BROKERS) задаётся в оверлее через extraEnvs.
|
||||
alternateConfig:
|
||||
extensions:
|
||||
health_check: {endpoint: "${env:MY_POD_IP}:13133"}
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "${env:MY_POD_IP}:4317"
|
||||
max_recv_msg_size_mib: 16
|
||||
processors:
|
||||
memory_limiter: {check_interval: 1s, limit_mib: 400, spike_limit_mib: 100}
|
||||
transform/access:
|
||||
error_mode: ignore
|
||||
log_statements:
|
||||
- context: log
|
||||
statements:
|
||||
# labels из envoyOtelAls приходят атрибутами — делаем из них тело (в Kafka уходит body как JSON)
|
||||
- set(body, attributes)
|
||||
- set(body["sarex_contour"], "${env:CONTOUR}")
|
||||
# Envoy пишет "-" для пустых значений
|
||||
- set(body["user_sub"], "") where body["user_sub"] == "-"
|
||||
- set(body["user_org"], "") where body["user_org"] == "-"
|
||||
- set(body["trace_id"], "") where body["trace_id"] == "-"
|
||||
# маскирование: токены в самом пути (публичные ссылки и т. п.)
|
||||
- replace_pattern(body["path"], "(?i)(token|secret|key|signature)/[^/]+", "$$1/***")
|
||||
- replace_pattern(body["path"], "[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{10,}", "***jwt***")
|
||||
# классификация
|
||||
- set(body["sarex_action"], "read") where body["method"] == "GET" or body["method"] == "HEAD" or body["method"] == "OPTIONS"
|
||||
- set(body["sarex_action"], "mutate") where body["method"] == "POST" or body["method"] == "PUT" or body["method"] == "PATCH" or body["method"] == "DELETE"
|
||||
- set(body["sarex_outcome"], "applied") where Int(body["response_code"]) >= 200 and Int(body["response_code"]) < 400
|
||||
- set(body["sarex_outcome"], "denied_authn") where body["response_code"] == "401"
|
||||
- set(body["sarex_outcome"], "denied_authz") where body["response_code"] == "403"
|
||||
- set(body["sarex_outcome"], "invalid") where Int(body["response_code"]) >= 400 and Int(body["response_code"]) < 500 and body["response_code"] != "401" and body["response_code"] != "403"
|
||||
- set(body["sarex_outcome"], "error") where Int(body["response_code"]) >= 500 or body["response_code"] == "0"
|
||||
- set(body["sarex_actor_type"], "anonymous")
|
||||
- set(body["sarex_actor_type"], "human") where body["user_sub"] != ""
|
||||
batch: {send_batch_size: 1000, send_batch_max_size: 2000, timeout: 2s}
|
||||
exporters:
|
||||
kafka/access:
|
||||
brokers: ["${env:KAFKA_BROKERS}"]
|
||||
protocol_version: 2.8.0
|
||||
topic: security.mesh-access
|
||||
encoding: raw # в сообщение — только body (JSON записи), без OTLP-обёртки
|
||||
auth:
|
||||
sasl: {mechanism: SCRAM-SHA-512, username: "${file:/vault/secrets/kafka-username}", password: "${file:/vault/secrets/kafka-password}"}
|
||||
tls: {insecure: false, ca_file: /vault/secrets/kafka-ca.crt}
|
||||
producer: {required_acks: -1, compression: zstd, max_message_bytes: 1048576}
|
||||
# Очередь в памяти (диска у Deployment нет): при падении пода неотправленные записи теряются.
|
||||
sending_queue: {enabled: true, queue_size: 50000, num_consumers: 4}
|
||||
retry_on_failure: {enabled: true, initial_interval: 5s, max_interval: 60s, max_elapsed_time: 0}
|
||||
service:
|
||||
extensions: [health_check]
|
||||
pipelines:
|
||||
logs/access:
|
||||
receivers: [otlp]
|
||||
processors: [memory_limiter, transform/access, batch]
|
||||
exporters: [kafka/access]
|
||||
telemetry:
|
||||
logs: {level: info}
|
||||
metrics: {address: "${env:MY_POD_IP}:8888"}
|
||||
@ -100,6 +100,9 @@ spec:
|
||||
- {name: audit-log, mountPath: /var/log/audit, readOnly: true}
|
||||
- {name: otelcol-storage, mountPath: /var/lib/otelcol-audit}
|
||||
|
||||
# Метка исключает под из webhook'а инъекции Istio ещё до мутации (namespace без istio-injection).
|
||||
podLabels:
|
||||
sidecar.istio.io/inject: "false"
|
||||
podAnnotations:
|
||||
sidecar.istio.io/inject: "false"
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
|
||||
@ -4,4 +4,6 @@ namespace: security-audit
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- networkpolicy.yaml
|
||||
- networkpolicy-access-collector.yaml
|
||||
- helmrelease-collector.yaml
|
||||
- helmrelease-access-collector.yaml
|
||||
|
||||
@ -7,4 +7,6 @@ metadata:
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
pod-security.kubernetes.io/warn: baseline
|
||||
pod-security.kubernetes.io/audit: baseline
|
||||
istio-injection: disabled
|
||||
# Без istio-injection: инъекция только по метке пода sidecar.istio.io/inject=true
|
||||
# (webhook object.sidecar-injector.istio.io). Метка disabled отключила бы и её.
|
||||
# Sidecar получает access-collector (п.30); audit-collector — нет.
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
# Дополняет общую audit-collector (DNS, Kafka, Vault, 13133): политики складываются.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: access-collector
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
security-audit/collector: access
|
||||
policyTypes: [Ingress, Egress]
|
||||
ingress:
|
||||
# OTLP gRPC только от ingress-шлюза. Селектор шлюза — заглушка, контур задаёт его патчем оверлея;
|
||||
# без патча не совпадёт ни с одним подом (закрыто по умолчанию).
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels: {kubernetes.io/metadata.name: istio-system}
|
||||
podSelector:
|
||||
matchLabels: {security-audit/gateway: unset}
|
||||
ports:
|
||||
- {port: 4317, protocol: TCP}
|
||||
# istio-proxy: 15020 — переписанные probe'ы приложения (kubelet), 15021 — readiness самого sidecar.
|
||||
- ports:
|
||||
- {port: 15020, protocol: TCP}
|
||||
- {port: 15021, protocol: TCP}
|
||||
egress:
|
||||
# xDS и сертификаты istio-proxy от istiod.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels: {kubernetes.io/metadata.name: istio-system}
|
||||
podSelector:
|
||||
matchLabels: {app: istiod}
|
||||
ports:
|
||||
- {port: 15012, protocol: TCP}
|
||||
11
infrastructure/security-audit/mesh/kustomization.yaml
Normal file
11
infrastructure/security-audit/mesh/kustomization.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Ресурсы шлюза Istio. Намеренно без поля namespace: base модуля переписал бы его на security-audit,
|
||||
# а этим ресурсам нужно оставаться в istio-system. Подключается из оверлея контура как resource,
|
||||
# селектор шлюза (заглушка security-audit/gateway: unset) патчится там.
|
||||
#
|
||||
# RequestAuthentication (пользователь в журнале — x-sarex-audit-sub/org) здесь пока нет: не подтверждены все
|
||||
# издатели JWT на шлюзе, а неизвестный издатель даёт 401. См. README, «Журнал доступа шлюза».
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- strip-audit-headers.yaml
|
||||
- telemetry.yaml
|
||||
32
infrastructure/security-audit/mesh/strip-audit-headers.yaml
Normal file
32
infrastructure/security-audit/mesh/strip-audit-headers.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
# Удаляет входящие x-sarex-audit-*: клиент не должен подставлять себе пользователя в журнал доступа.
|
||||
# Стоит первым HTTP-фильтром (INSERT_FIRST) — раньше jwt_authn, который заполняет эти заголовки из claims.
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: EnvoyFilter
|
||||
metadata:
|
||||
name: strip-audit-headers
|
||||
namespace: istio-system
|
||||
spec:
|
||||
workloadSelector:
|
||||
labels:
|
||||
security-audit/gateway: unset
|
||||
configPatches:
|
||||
- applyTo: HTTP_FILTER
|
||||
match:
|
||||
context: GATEWAY
|
||||
listener:
|
||||
filterChain:
|
||||
filter:
|
||||
name: envoy.filters.network.http_connection_manager
|
||||
patch:
|
||||
operation: INSERT_FIRST
|
||||
value:
|
||||
name: security-audit.strip-audit-headers
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
default_source_code:
|
||||
inline_string: |
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
headers:remove("x-sarex-audit-sub")
|
||||
headers:remove("x-sarex-audit-org")
|
||||
end
|
||||
17
infrastructure/security-audit/mesh/telemetry.yaml
Normal file
17
infrastructure/security-audit/mesh/telemetry.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
# Включает провайдер audit-access (meshConfig.extensionProviders в istiod) на ingress-шлюзе.
|
||||
# Провайдер должен быть раскатан раньше: иначе istiod не найдёт его и журнал не включится.
|
||||
apiVersion: telemetry.istio.io/v1
|
||||
kind: Telemetry
|
||||
metadata:
|
||||
name: gateway-audit-access
|
||||
namespace: istio-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
security-audit/gateway: unset
|
||||
accessLogging:
|
||||
- providers:
|
||||
- name: audit-access
|
||||
# Только health-check'и. Статику не исключаем — решение за ИБ.
|
||||
filter:
|
||||
expression: '!(request.url_path.startsWith("/healthz") || request.url_path.startsWith("/ready"))'
|
||||
17
infrastructure/security-audit/uralkal/access-collector.yaml
Normal file
17
infrastructure/security-audit/uralkal/access-collector.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: access-collector
|
||||
namespace: security-audit
|
||||
spec:
|
||||
dependsOn:
|
||||
- name: kafka
|
||||
namespace: kafka
|
||||
- name: vault
|
||||
namespace: vault
|
||||
values:
|
||||
extraEnvs:
|
||||
- name: CONTOUR
|
||||
value: uralkal
|
||||
- name: KAFKA_BROKERS
|
||||
value: kafka-kafka-contour.kafka.svc.cluster.local:9092
|
||||
@ -2,5 +2,30 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../base
|
||||
- ../mesh # istio-system; применять после раскатки провайдера audit-access в istio-pilot/uralkal
|
||||
patches:
|
||||
- path: audit-collector.yaml
|
||||
- path: access-collector.yaml
|
||||
# Селектор ingress-шлюза (HelmRelease istio-system/ingressgateway): replace, а не merge —
|
||||
# merge-patch CRD оставил бы и заглушку security-audit/gateway.
|
||||
- target:
|
||||
kind: EnvoyFilter
|
||||
name: strip-audit-headers
|
||||
patch: |-
|
||||
- op: replace
|
||||
path: /spec/workloadSelector/labels
|
||||
value: {istio: ingressgateway}
|
||||
- target:
|
||||
kind: Telemetry
|
||||
name: gateway-audit-access
|
||||
patch: |-
|
||||
- op: replace
|
||||
path: /spec/selector/matchLabels
|
||||
value: {istio: ingressgateway}
|
||||
- target:
|
||||
kind: NetworkPolicy
|
||||
name: access-collector
|
||||
patch: |-
|
||||
- op: replace
|
||||
path: /spec/ingress/0/from/0/podSelector/matchLabels
|
||||
value: {istio: ingressgateway}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user