
Привет, Хабр! Меня зовут Валентин Вертелецкий, я DevOps в СберТехе, занимаюсь развитием Platform V Kintsugi — это графическая консоль для сопровождения Postgres-like СУБД. Наш продукт построен на микросервисной архитектуре и сначала разрабатывался с использованием базовой функциональности Kubernetes — там нет встроенных механизмов аутентификации, авторизации, управления доступом и шифрования трафика. Когда же у нас стало больше сервисов, нам понадобилось повысить защиту и отказоустойчивость, добавить возможности управления доступом.
Мы опираемся на подход Zero Trust: ни одному элементу системы не доверяем по умолчанию. Каждый запрос проверяется, привилегии для администраторов минимальны, трафик валидируется и шифруется. Нам предстояло обеспечить надёжную аутентификацию и авторизацию, а также централизованный контроль и мониторинг запросов. В этом нам помогла технология Service Mesh.
Для управления микросервисами в Kubernetes мы используем Platform V Synapse Service Mesh от СберТеха — это решение на основе платформы Istio. Покажу, как всё работает у нас. Плюс, я подготовил демо-проект для тестирования кейсов (ссылка в конце статьи). Надеюсь, он будет полезен командам, работающим с микросервисами.
Service Mesh (сервисная сеть) — это технология для управления сервисами в распределённых системах и микросервисной архитектуре. Её ключевые компоненты:
Sidecar Proxy: запускается вместе с каждым сервисом в виде отдельного контейнера (sidecar). Этот контейнер обрабатывает весь входящий и исходящий трафик сервиса, выполняя задачи, связанные с управлением сетью и безопасностью;
Control Plane: управляет прокси-сервисами, координирует их работу и отвечает за распределение конфигураций, политику безопасности, управление трафиком;
Data Plane: включает в себя сами прокси-сервисы, которые непосредственно занимаются обработкой трафика между сервисами.
Наши условия. Как я уже упомянул, наш продукт базируется на Platform V Synapse Service Mesh. Это Service Mesh от СберТеха, разработанный на основе Istio. Istio — open source-платформа для управления трафиком и защиты микросервисов в распределённых системах. В качестве проксирующего компонента используется Envoy — высокопроизводительный прокси с открытым исходным кодом.
Назначение Istio:
повышение безопасности: шифрование трафика, аутентификация, авторизация;
управление трафиком: маршрутизация запросов, балансировка нагрузки, отказоустойчивость;
наблюдаемость: мониторинг производительности сервисов, сбор метрик, трассировка, журналирование;
отказоустойчивость: механизм таймаутов, повторных попыток выполнения запросов.
Мы используем архитектуру Istio в режиме sidecar (sidecar mode), когда каждый сервис в облаке разворачивается вместе с контейнером экземпляра Envoy-прокси, который выступает посредником между приложением и внешним миром. Задача проксирующего узла — обработка входящего и исходящего сервисного трафика. Так в архитектуре внедряется дополнительный слой абстракции — он прозрачен для прикладного сервиса и позволяет решать инфраструктурные задачи, не вовлекая в этот процесс само приложение.

Для добавления sidecar-контейнера Envoy-прокси в под с приложением используется следующая инструкция в блоке annotations
:
sidecar.istio.io/inject: 'true'
Теперь можно приступить к настройке. В Istio для управления прокси-контейнером применяются специализированные ресурсы — Custom Resource Definition (CRD), интерфейсы для конфигурации Envoy-прокси.
Основные типы ресурсов для конфигурирования Envoy, про которые расскажу:
ServiceEntry
: используется для регистрации сервисов в Istio service registry;VirtualService
: управляет политиками определения маршрутов трафика к конкретному сервису; включает в себя правила маршрутизации, балансировки и другие политики обработки сервисного трафика;Gateway
: определяет точку входа внешнего трафика в сервисную сеть и указывает порты и протоколы, которые будут использоваться для передачи трафика;DestinationRule
: используется для определения подмножества сервисов (subsets), позволяет управлять политиками подключения, балансировки и настройки шифрования трафика для этих подмножеств;EnvoyFilter
: ресурс для более тонкой настройки Envoy-прокси, позволяет создавать дополнительные фильтры для уже существующей конфигурации;PeerAuthentication
: для определения политик аутентификации между сервисами;AuthorizationPolicy
: определяет политики авторизации на уровне сервисов, позволяет контролировать доступ к ресурсам сервиса на основе различных критериев.
Примечание: примеры конфигураций, описанные в статье, протестированы в кластере Synapse Service Mesh 3.9 (Istio Service Mesh 1.17).
Итак, какие же возможности предлагает Istio для обеспечения безопасного транспорта и контроля над потоками трафика внутри и вне микросервисной архитектуры?
Концепция граничных шлюзов в Istio Service Mesh
В Service Mesh для улучшения наблюдаемости и безопасности сервисов используется концепция граничных шлюзов. Что это даёт?
Централизованное управление доступом. Граничные шлюзы служат единственной точкой входа и выхода для всех внешних запросов. Это упрощает внедрение политик безопасности и управление трафиком.
Наблюдаемость. Через шлюзы собирается информация о входящих и исходящих запросах. Позволяет отслеживать производительность, выявлять узкие места и анализировать поведение системы.
Безопасность. Шлюзы обеспечивают шифрование трафика, защиту от атак (например, DDoS) и возможность аутентификации пользователей и сервисов.
Маршрутизация. Можно динамически направлять запросы к нужным сервисам, поддерживая различные стратегии балансировки нагрузки и распределения трафика.
Граничные шлюзы в Istio — это специализированные прокси-серверы, они делятся на два типа:
Ingress Gateway
— для обработки входящего трафика (от клиентов или других систем) в микросервисную сеть.Egress Gateway
контролирует исходящий трафик — запросы из микросервисной сети наружу (например, внешние API, базы данных или другие сервисы).
Дополним нашу схему представленными компонентами и рассмотрим их назначение и подходы к конфигурированию.

Входящий граничный шлюз Ingress Gateway
Ingress Gateway
— единая точка входа для внешнего трафика, поступающего в защищённый контур. Разворачивается как отдельный компонент в Kubernetes, работает как обратный прокси-сервер. Все входящие запросы проходят через него, прежде чем попасть к внутренним микросервисам. Ingress Gateway
отвечает за маршрутизацию, первичную фильтрацию и контроль доступа.
Функциональное назначение:
единая точка управления: управление всеми входящими запросами через один узел позволяет централизованно настраивать правила маршрутизации, безопасность, аутентификацию и мониторинг;
упрощение администрирования и контроля трафика: вместо взаимодействия с каждым отдельным сервисом все поступающие из внешнего мира запросы обрабатываются на одном узле;
безопасность: граничный шлюз выступает в качестве первого рубежа обороны, снижая поверхность атаки и реализуя функции проверки подлинности, шифрования трафика, предотвращения атак и фильтрации нежелательных запросов;
оптимизация ресурсов: управление запросами через единый шлюз позволяет оптимизировать распределение нагрузки и лучше контролировать пропускную способность системы.
Пример. Рассмотрим, как настраивается проксирование трафика через Ingress Gateway
к одному из прикладных сервисов Kintsugi, который отвечает за управление объектами наблюдаемых баз данных в Kintsugi.
Создадим конфигурацию сервиса, которая позволит перенаправлять входящий трафик на порт нашего контейнера с приложением, запущенным в поде с sidecar-прокси:
---
apiVersion: v1
kind: Service
metadata:
name: curator
spec:
selector:
app: curator
ports:
- name: tcp-curator
protocol: TCP
port: 8080
targetPort: 8080
Сервис с именем curator
принимает подключения через порт 8080 (TCP)
и перенаправляет трафик на порт нашего приложения.
Далее опишем конфигурацию сервиса для граничного прокси, который будет принимать все приходящие снаружи запросы:
---
kind: Service
apiVersion: v1
metadata:
name: ingressgateway
labels:
istio: ingressgateway
spec:
ports:
- name: https
protocol: TCP
port: 443
targetPort: 8443
selector:
istio: ingressgateway
Все подключения к сервису ingressgateway
через порт 443 (TCP)
будут перенаправляться на порт 8443 (TCP)
, где Envoy-прокси в поде Ingress-шлюза слушает входящие подключения.
Теперь, когда у нас обеспечена сетевая доступность наших ключевых сервисов, приступим к настройке Istio. Определим точку входа для внешнего трафика с использованием Gateway
:
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
labels:
app: curator
name: curator-gw
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- curator-da-dp01-db-kintsugi-master.solution.test
port:
name: https
number: 8443
protocol: HTTPS
tls:
caCertificates: /secrets/istio/ingressgateway-ca-certs/ca.crt
mode: SIMPLE
privateKey: /secrets/istio/ingressgateway-certs/tls.key
serverCertificate: /secrets/istio/ingressgateway-certs/tls.crt
Приведённая конфигурация создаёт Gateway
, который принимает входящий HTTPS-трафик на порт 8443
для хоста curator-da-dp01-db-kintsugi-master.solution.test
. Трафик обрабатывается с использованием TLS-шифрования. Сертификаты и ключи берутся из секретов, примонтированных в контейнер с Envoy-прокси.
Для описания маршрутизации трафика на уровне сервиса воспользуемся ресурсом VirtualService
:
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: curator-vs
labels:
app: curator
spec:
exportTo:
- .
gateways:
- curator-gw
hosts:
- curator-da-dp01-db-kintsugi-master.solution.test
http:
- route:
- destination:
host: curator
port:
number: 8080
Конфигурация VirtualService
определяет маршрутизацию HTTP-трафика, поступающего через Gateway
для хоста curator-da-dp01-db-kintsugi-master.solution.test
. Весь трафик прозрачно направляется на физический сервис, принимающий подключения через порт 8080
. Параметр exportTo
указывает, что правило применится только для текущего пространства имён. Рекомендуется использовать именно такую конфигурацию, чтобы при отладке не повлиять на работоспособность других проектов, подключённых к контрольной панели Istio.
Напоследок применим конфигурацию, которая создаёт внешний маршрут к сервису curator
через единую точку входа:
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: curator
labels:
istio: ingressgateway
spec:
ingressClassName: nginx
tls:
- hosts:
- curator-da-dp01-db-kintsugi-master.solution.test
secretName: istio-ingressgateway-certs
rules:
- host: curator-da-dp01-db-kintsugi-master.solution.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ingressgateway
port:
number: 443
Трафик, поступающий на хост curator-da-dp01-db-kintsugi-master.solution.test
и порт 443
, будет перенаправлен на созданный нами сервис ingressgateway
, где к нему применятся все политики, описанные в наших ресурсах.
Выполним запрос к ресурсу readiness
нашего сервиса и посмотрим лог граничного прокси:
sh-4.4$ curl -I -X GET --http1.1 --cacert ca_bundle.crt https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
date: Fri, 16 May 2025 20:28:10 GMT
server: istio-envoy
content-length: 0
Лог Ingress Gateway
:
[2025-05-16T20:28:10.323Z] "GET /readiness HTTP/1.1" 200 - via_upstream - "-" 0 0 23 23 "172.21.9.138" "curl/7.61.1" "f15176bd-fa4e-41b9-8a09-142021d9cbaf" "curator-da-dp01-db-kintsugi-master.solution.test" "172.21.13.146:8080" outbound|8080||curator.da-dp01-db-kintsugi-master.svc.cluster.local 172.21.14.182:55290 172.21.14.182:8443 172.21.9.138:10662 curator-da-dp01-db-kintsugi-master.solution.test - 3363
Значение |
Интерпретация |
|
Время запроса (UTC) |
|
Метод, путь запроса и протокол |
|
HTTP-код ответа (успешно) |
|
Размер ответа (тело запроса отсутствует) |
|
Запрос обработан upstream-сервером |
|
Upstream service time (не указано) |
|
Referrer (отсутствует) |
|
Размер тела запроса и ответа (тело запроса отсутствует) |
|
23 мс — время между приёмом первого байта запроса и отправкой последнего байта ответа сервером; 23 мс — общее время выполнения запроса. |
|
IP-адрес клиента, инициировавшего запрос |
|
User Agent клиента |
|
Request ID — уникальный идентификатор запроса |
|
Запрашиваемый хост |
|
Адрес конечного сервиса, куда был спроксирован запрос |
|
Описание направления запроса внутри кластера: |
|
Внутренний интерфейс контейнера, обрабатывающего запрос |
|
Публичный интерфейс контейнера |
|
Внешний интерфейс клиента, отправившего запрос через curl |
|
Внешний домен приложения, видимый снаружи кластера |
|
Дополнительные метаданные запроса (данные отсутствуют) |
|
Размер лог-записи в байтах |
Согласно данным из лога, клиент отправил GET
-запрос к ресурсу readiness
приложения curator-da-dp01-db-kintsugi-master
. Время выполнения запроса составило 23 мс, операция завершилась успехом.
Теперь все запросы к нашему прикладному сервису фиксируются на граничном шлюзе. Это позволяет нам детально отслеживать и контролировать внешний трафик, поступающий в защищённый контур. Можем быстро выявлять аномалии, анализировать нагрузку и принимать меры для оптимизации безопасности и производительности. Profit!
Граничный исходящий шлюз Egress Gateway
Помимо обработки входящих запросов часто возникает необходимость организовать доступ микросервисов к другим внешним ресурсам за пределами сервисной сети, например, к СУБД, системам хранения логов и так далее. Для таких задач в Istio предусмотрен отдельный компонент — Egress Gateway
. Это единая точка выхода трафика из защищённого контура — шлюз, через него все запросы, исходящие от внутренних сервисов, направляются во внешние системы или Интернет.
Функциональное назначение:
контроль и управление: можно централизованно контролировать то, куда и как отправляются все запросы;
повышение безопасности: внешние ресурсы — потенциальный источник угроз, и централизованный контроль исходящего трафика помогает защитить внутреннюю сеть от несанкционированного доступа или утечек данных;
оптимизация: управление трафиком через единую точку выхода позволяет эффективнее использовать сетевую инфраструктуру и ресурсы системы.
Перед тем как перейти к разбору проксирования трафика через Egress Gateway
, коротко расскажу о политиках обработки исходящего трафика. Для управления доступом к внешним сервисам в Istio Service Mesh на глобальном уровне используется ресурс IstioOperator
. Это один из возможных способов конфигурирования политик безопасности. Он позволяет управлять различными аспектами конфигурирования, включая настройку сетевых политик для исходящего трафика. В частности, параметр spec.meshConfig.outboundTrafficPolicy.mode
контролирует то, как сервисы в кластере взаимодействуют с внешними сервисами за пределами сервисной сети. Здесь предлагается два режима:
ALLOW_ANY
: разрешает исходящие запросы ко всем внешним сервисам без ограничений. Может применяться в архитектуре, где политики Istio используются для внутренних сервисов, но взаимодействие с внешними источниками остаётся полностью открытым.REGISTRY_ONLY
: разрешает исходящие запросы только внутри сервисной сети. Все обращения к внешним ресурсам, не зарегистрированным в Istio, блокируются.
---
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
name: istio-default
...
spec:
meshConfig:
outboundTrafficPolicy:
mode: REGISTRY_ONLY
...
Ставим режим, ограничивающий взаимодействие компонентов, входящих в нашу сервисную сеть, с внешними сервисами, не зарегистрированными в Istio. Так мы обеспечим строгий контроль трафика, исходящего из сервисной сети.
Переходим к реализации сценария взаимодействия компонентов сервисной сети с внешними сервисами, используя Egress Gateway
в качестве прямого прокси.
Классический пример взаимодействия Platform V Kintsugi с внешними сервисами — это интеграция с экземплярами СУБД снаружи кластера. Данные о наблюдаемых кластерах и объектах мониторинга Kintsugi хранит в репозитории метаданных на основе PostgreSQL. Воспользуемся ресурсом ServiceEntry
для регистрации внешнего сервиса СУБД в Istio и создадим разрешающее правило для передачи трафика из сервисной сети наружу:
---
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: repository-se
spec:
addresses:
- 10.40.20.50
exportTo:
- .
hosts:
- repository.solution.test
location: MESH_EXTERNAL
ports:
- name: tcp-5433
number: 5433
protocol: tcp
resolution: STATIC
Основные параметры конфигурации ресурса ServiceEntry
:
hosts
: список хостов (доменные имена или IP-адреса), которые будут использоваться для внешних сервисов;ports
: список портов и протоколов внешних сервисов;-
location
: указывает месторасположение сервиса:MESH_INTERNAL
: сервис находится внутри сети Istio;MESH_EXTERNAL
: сервис находится снаружи сети Istio;
resolution
: параметр определяет способ разрешения адреса внешнего сервиса;addresses
: используется для статического определения конечных точек (адресов) внешних сервисов.
В приведённой конфигурации создаётся запись в реестре Istio для внешнего сервиса PostgreSQL — он зарегистрирован под статически указанным доменным именем repository.solution.test
(IP: 10.40.20.50
) и принимает подключения через порт 5433 (TCP)
. С этого момента трафик к этому сервису будет отслеживаться и управляться Istio.
Для реализации проксирования трафика через Egress Gateway
от нашего сервиса к СУБД создадим точку проксирования трафика в граничном прокси:
---
kind: Service
apiVersion: v1
metadata:
name: egress-repository-service
labels:
istio: egressgateway
spec:
ports:
- name: tcp-passthrough
protocol: TCP
port: 5000
targetPort: 5000
selector:
istio: egressgateway
В конфигурации определим порт 5000 (TCP)
, через который в Egress Gateway
будут обрабатываться подключения наших внутренних сервисов к СУБД.
Как и в случае с входящим граничным прокси, с помощью Gateway
и VirtualService
добавим политики маршрутизации исходящего трафика:
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: repository-gw
spec:
selector:
istio: egressgateway
servers:
- hosts:
- repository.solution.test
port:
name: tcp-passthrough
number: 5000
protocol: TCP
Приведённая конфигурация создаёт ресурс Gateway
, который принимает входящий TCP-трафик через порт 5000
для хоста repository.solution.test
(IP: 10.40.20.50
). Трафик обрабатывается в режиме passthrough: между микросервисом и СУБД настраивается защищённый канал передачи данных со сквозным шифрованием.
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: repository-vs
spec:
exportTo:
- .
gateways:
- repository-gw
- mesh
hosts:
- repository.solution.test
tcp:
- match:
- gateways:
- mesh
port: 5433
route:
- destination:
host: egress-repository-service
port:
number: 5000
subset: repository-internal
- match:
- gateways:
- repository-gw
port: 5000
route:
- destination:
host: repository.solution.test
port:
number: 5433
subset: tcp-passthrough-repository
Созданный VirtualService
управляет маршрутизацией TCP-трафика: все запросы, адресованные хосту СУБД, зарегистрированному в Istio под именем repository.solution.test на порт 5433 (TCP)
, через сервисную сеть перенаправляются на внутренний порт 5000 (TCP)
граничного прокси. После чего трафик, поступающий на порт 5000 (TCP)
граничного прокси, маршрутизируется на адрес хоста repository.solution.test
(IP: 10.40.20.50
) и порт 5433 (TCP)
. В правилах маршрутизации дифференцируем трафик по порту назначения и ресурсу Gateway
, где:
repository-gw
: шлюз, созданный на предыдущем шаге и принимающий подключения кEgress Gateway
;mesh
: специальное зарезервированное значение в Istio. Используется для обозначения внутреннего трафика между микросервисами в сервисной сети. Указывает, что соответствующее правило маршрутизации должно применяться к трафику, циркулирующему напрямую между сервисами без проксирования через граничные шлюзы Ingress и Egress.
Для удобства мониторинга и отладки перенаправим исследуемые потоки трафика в выделенные subsets.
В конце определим ресурсы правила DestinationRule
для каждого потока трафика:
internal
: трафик от микросервиса кEgress Gateway
(subsetrepository-internal
);external
: трафик отEgress Gateway
к сервису СУБД (subsettcp-passthrough-repository
).
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: repository-internal-dr
spec:
exportTo:
- .
host: egress-repository-service
subsets:
- name: repository-internal
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: repository-external-dr
spec:
exportTo:
- .
host: repository.solution.test
subsets:
- name: tcp-passthrough-repository
workloadSelector:
matchLabels:
istio: egressgateway
Так мы получили схему проксирования трафика через граничный шлюз Egress Gateway
.

Воспользуемся вспомогательным инструментом и отправим запрос из сервисной сети к внешнему сервису:
sh-4.4$ curl -v repository.solution.test:5433
* Rebuilt URL to: repository.solution.test:5433/
* Trying 10.40.20.50...
* TCP_NODELAY set
* Connected to repository.solution.test (10.40.20.50) port 5433 (#0)
[2025-05-16T20:35:53.303Z] "- - -" 0 - - - "-" 94 0 7 - "-" "-" "-" "-" "10.40.20.50:5433" outbound|5433|tcp-passthrough-repository|repository.solution.test 172.21.28.154:39058 172.21.28.154:5000 172.21.13.146:37142 - - 246
Примечание: в логе отсутствуют данные, специфичные для HTTP-трафика.
Значение |
Интерпретация |
|
Время запроса (UTC) |
|
Метод, путь запроса и протокол отсутствуют |
|
HTTP-код ответа отсутствует, значение по умолчанию 0 |
|
Размер ответа, тело запроса отсутствует |
|
Не используется |
|
Upstream service time не указано |
|
Referrer отсутствует |
|
Размер тела запроса и ответа (тело запроса отсутствует) |
|
7 мс — время между приёмом первого байта запроса и отправкой последнего байта ответа сервером. Данные о продолжительности запроса отсутствуют. |
|
Адрес downstream отсутствует или не определён |
|
User Agent клиента отсутствует |
|
Request ID отсутствует |
|
Запрашиваемый хост отсутствует |
|
Адрес конечного upstream-сервиса, куда был спроксирован запрос |
|
Описание направления запроса внутри кластера:
|
|
Внутренний интерфейс контейнера |
|
Публичный интерфейс контейнера |
|
Внешний интерфейс, куда |
|
Не используется |
|
Дополнительные метаданные запроса (данные отсутствуют) |
|
Размер лог-записи в байтах |
Согласно данным из лога, изнутри кластера успешно установлено TCP-соединение с сервисом repository.solution.test
.
В результате исходящие запросы от нашего приложения ко внешнему сервису СУБД фиксируются на граничном шлюзе Egress Gateway
. Благодаря чему у нас появляется возможность контролировать сетевое взаимодействие в egress-сегменте нашей сервисной сети.
Разделяем и делегируем
Теперь мы можем контролировать исходящий трафик на уровне Egress Gateway
. Но что нам это даёт помимо улучшенного мониторинга? И можно ли как-то использовать это при взаимодействии с другими сервисами?
Как и sidecar-прокси, расположенный в поде с контейнером приложения, Egress Gateway
служит посредником при коммуникации компонентов сервисной сети с внешними сервисами. В предыдущем примере мы рассмотрели взаимодействие нашего компонента с внешним сервисом СУБД, когда клиент использует сквозное защищённое подключение к БД в режиме passthrough и весь трафик прозрачно проксируется через граничный шлюз. Теперь другой пример. Есть задача по доставке логов приложений во внешнее хранилище данных. Для обеспечения защищённого соединения клиент использует сертификат, приватный ключ и цепочку сертификатов доверенных удостоверяющих центров. В сервисной сети с небольшим количеством компонентов задача решается тривиально, но когда они начинают исчисляться десятками или сотнями, конфигурирование и управление сервисами усложняется. Что можно предпринять в таком случае?
Отдадим реализацию безопасного канала связи с внешним сервисом самому граничному шлюзу, так мы освободим прикладные сервисы от инфраструктурных задач.

На схеме представлено взаимодействие прикладного сервиса с внешним ресурсом с использованием защищённого транспорта. Немного позже вернёмся к вопросу шифрования трафика внутри контура сервисной сети. А пока перейдём к настройке граничного шлюза в части его взаимодействия с внешним сервисом Elasticsearch, которое возьмём в качестве примера для хранения логов приложения.
Для простоты положим, что все необходимые артефакты — сертификат клиента, приватный ключ и цепочка сертификатов доверенных удостоверяющих центров — смонтированы в файловую систему Egress Gateway
, и мы сразу же можем перейти к конфигурированию Istio. В качестве отправной точки на граничном исходящем шлюзе выполним запрос к внешнему сервису. Убедимся в том, что он доступен и у нас есть всё необходимое для реализации защищённого транспорта. В качестве примера используем запрос проверки состояния кластера Elasticsearch:
sh-4.4$ curl -X GET --cacert /secrets/istio/egressgateway-ca-certs/ca.crt --http1.1 -I https://elastic.solution.test:9200/_cluster/health
HTTP/1.1 200 OK
Server: nginx/1.27.2
Content-Type: application/json
Content-Length: 390
Connection: keep-alive
X-elastic-product: Elasticsearch
Strict-Transport-Security: max-age=31536000
Поскольку граничный шлюз взаимодействует с внешними сервисами напрямую, здесь нам не требуется дополнительного конфигурирования проксирования. Достаточно воспользоваться уже имеющимся набором данных для безопасного подключения к внешнему сервису Elasticsearch.
Проверочный запрос выполнен успешно, можно переходить к настройке проксирования трафика из сервисной сети.
Выполним очередной запрос, но уже из контейнера прикладного сервиса curator
, и убедимся, что у нас действительно отсутствует сетевая связность с хранилищем логов.
sh-4.4$ curl -X GET --http1.1 -I http://elastic.solution.test:9200/_cluster/health
curl: (56) Recv failure: Connection reset by peer
Получаем отказ в установлении соединения от удалённого сервиса. Поэтому переходим к настройке правил обработки трафика в Isito.
Первым делом зарегистрируем внешний сервис в сервисной сети:
---
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: elastic-se
spec:
exportTo:
- .
hosts:
- elastic.solution.test
location: MESH_EXTERNAL
ports:
- name: http-elastic
number: 8080
protocol: HTTP
- name: tls-elastic
number: 9200
protocol: TLS
resolution: DNS
Как ранее упоминалось, эта задача решается с помощью ServiceEntry
, где мы декларируем наш внешний ресурс с адресом elastic.solution.test
, принимающим запросы через порт 9200 (HTTPS)
. В этот раз Istio будет самостоятельно разрешать доменное имя узла с помощью DNS и выполнять маршрутизацию трафика на основе правил, указанных нами для этого хоста. Уточню: в нашей архитектуре прикладной сервис не отвечает за обеспечение безопасности, задача шифрования трафика возлагается на инфраструктуру — для этого в сервисной сети мы определим порт 8080 (HTTP)
для обработки таких запросов.
Создадим сервис для приёма трафика граничным шлюзом от узлов сервисной сети:
---
kind: Service
apiVersion: v1
metadata:
name: egress-elastic-service
labels:
istio: egressgateway
spec:
ports:
- name: tcp-elasticsearch
protocol: TCP
port: 4000
targetPort: 4000
selector:
istio: egressgateway
Теперь исходящий граничный шлюз готов принимать от нас подключения к порту 4000
через выделенный для нашей задачи сервис egress-elastic-service
.
Когда определены все базовые сущности для транспорта трафика, пришло время описать правила маршрутизации запросов, адресованных нашему внешнему сервису. Для этого определим в ресурсе Gateway
адрес и порт внешнего сервиса и с помощью VirtualService
свяжем его с правилом маршрутизации трафика:
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: elastic-gw
spec:
selector:
istio: egressgateway
servers:
- hosts:
- elastic.solution.test
port:
name: http-elasticsearch
number: 4000
protocol: HTTP
Конфигурация создаёт шлюз с именем elastic-gw
, его задача — перенаправлять HTTP-трафик, адресованный хосту elastic.solution.test
на граничный шлюз Egress Gateway
, который принимает подключения через порт 4000
.
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: elastic-vs
spec:
exportTo:
- .
gateways:
- elastic-gw
- mesh
hosts:
- elastic.solution.test
http:
- match:
- gateways:
- mesh
port: 8080
route:
- destination:
host: egress-elastic-service
port:
number: 4000
subset: elastic-internal
- match:
- gateways:
- elastic-gw
port: 4000
route:
- destination:
host: elastic.solution.test
port:
number: 9200
subset: tls-origination-elastic
В конфигурации VirtualService
определим два правила маршрутизации:
Запросы, приходящие на внутренний шлюз mesh и порт
8080
перенаправляются на порт4000
сервисаegress-elastic-service
. Для полноты контроля определим для этого потока трафика subsetelastic-internal
.Запросы, поступающие на шлюз
elastic-gw
и порт4000
перенаправляются на хостelastic.solution.test
и порт9200
. Трафик, соответствующий заданному набору критериев, выделим в subsettls-origination-elastic
.
Таким образом, запросы от внутренних сервисов к хосту с именем elastic.solution.test
через порт 8080 (HTTP)
маршрутизируются на шлюз Egress Gateway
и далее перенаправляются на целевой хост и порт внешнего сервиса.
Всё, что нам осталось — определить политики обработки для двух потоков трафика:
internal
: трафик кEgress Gateway
от внутреннего сервиса (subsetelastic-internal
);external
: трафик отEgress Gateway
к хранилищу данных Elasticsearch (subsettls-origination-elastic
).
Воспользуемся ресурсом DestinationRule
и опишем политику для каждого из потоков:
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: egress-elastic-internal-dr
spec:
exportTo:
- .
host: egress-elastic-service
subsets:
- name: elastic-internal
workloadSelector:
matchLabels:
app: curator
Конфигурация описывает политику обработки трафика, передаваемого от внутреннего сервиса curator
к исходящему граничному шлюзу. Здесь создаётся subset elastic-internal
, в котором трафик передаётся прозрачно без применения каких-либо дополнительных политик обработки. Применяем правило для ресурсов с меткой прикладного сервиса app: curator
.
На финальном шаге конфигурирования определим политику для потока трафика от граничного шлюза к внешнему сервису:
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: egress-elastic-external-dr
spec:
exportTo:
- .
host: elastic.solution.test
subsets:
- name: tls-origination-elastic
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
portLevelSettings:
- port:
number: 9200
tls:
caCertificates: /secrets/istio/egressgateway-ca-certs/ca.crt
clientCertificate: /secrets/istio/egressgateway-certs/tls.crt
mode: SIMPLE
privateKey: /secrets/istio/egressgateway-certs/tls.key
sni: elastic.solution.test
workloadSelector:
matchLabels:
istio: egressgateway
Политика определяет subset tls-origination-elastic
и позволяет установить защищённое подключения в режиме SIMPLE
с проверкой подлинности сертификата сервера. Так, все запросы, адресованные хосту elastic.solution.test
на порт 9200
, будут шифроваться на граничном шлюзе и отправляться внешнему сервису.
Теперь, когда конфигурация готова, проверим тестовым запросом её работу. Отправим запрос из сервисной сети к внешнему хранилищу Elasticsearch:
sh-4.4$ curl -X GET -u $USER:$PASSWORD --http1.1 -I http://elastic.solution.test:8080/_cluster/health
HTTP/1.1 200 OK
server: envoy
date: Fri, 16 May 2025 20:42:47 GMT
content-type: application/json
content-length: 390
x-elastic-product: Elasticsearch
strict-transport-security: max-age=31536000
x-envoy-upstream-service-time: 7
Лог Egress Gateway
:
[2025-05-16T20:42:47.162Z] "GET /_cluster/health HTTP/1.1" 200 - via_upstream - "-" 0 390 63 62 "172.21.13.146" "curl/7.61.1" "8e5ab966-d23b-90fb-b306-7d23021c2805" "elastic.solution.test:8080" "10.40.20.22:9200" outbound|9200|tls-origination-elastic|elastic.solution.test 172.21.28.154:46938 172.21.28.154:4000 172.21.13.146:38342 - - 253
Запрос выполнен успешно, это значит, что граничный шлюз обеспечивает прозрачное для прикладного сервиса защищённое взаимодействие с внешним ресурсом.
Архитектурная схема готова к приёму и обработке внешних пользовательских и сервисных запросов. Теперь пора поговорить об аутентификации и авторизации. Ниже приведу примеры их использования в консоли управления БД Kintsugi.
Аутентификация
Istio обеспечивает проверку подлинности клиентов, отправляющих запросы к сервисам внутри сервисной сети. Поддерживает два основных механизма аутентификации:
сетевая аутентификация (mTLS-аутентификация);
JWT-аутентификация.
Рассмотрим каждый из них подробно.
Шифрование данных при межсервисном взаимодействии и сетевая аутентификация
Mutual TLS (mTLS) — расширение стандартного протокола шифрования TLS, где не только сервер аутентифицирует клиента, но и клиент проверяет подлинность сервера. Это повышает безопасность взаимодействия между сервисами.
Рассмотрим работу механизма в действии. Но сперва проверим, в каком виде данные передаются внутри сервисной сети без включённой политики сетевой аутентификации. Отправим тестовый запрос из сервисной сети к нашему прикладному сервису curator
и выполним дамп сетевого трафика на внешнем интерфейсе пода:
sh-4.4$ curl -i -X POST curator:8080 -d '{"message": "Test Kintsugi"}'
HTTP/1.1 200 OK
server: envoy
content-length: 22
content-type: application/json
С помощью curl отправим «тайное» послание сервису изнутри сети Istio и параллельно посмотрим, что об этом думает tcpdump
:
sh-4.4$ tcpdump -A -i eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

Как мы видим, трафик передаётся в незашифрованном виде, поэтому перейдём к настройке шифрования и взаимной аутентификации в нашей сервисной сети. Обратимся к ресурсу PeerAuthentication
, который управляет настройками аутентификации на уровне сервиса. Он определяет требования к аутентификации между клиентом и сервером в рамках одного пространства имён или всего кластера. Рассмотрим базовый вариант его конфигурации:
---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
spec:
mtls:
mode: STRICT
spec.mtls.mode
— ключевой параметр ресурса, который определяет требования к режиму работы mTLS для входящих запросов и может принимать следующие значения:
UNSET
: наследуется режим от родительской политики, например, глобальной настройки сервисной сети;DISABLE
: mTLS отключен, трафик передаётся в открытом виде (plain-text);PERMISSIVE
: разрешает использовать как plain-text, так и mTLS-соединение;STRICT
: допускает использование только mTLS-соединения.
В нашем примере будем строго требовать возможность использовать режим mTLS для всех входящих запросов. Это правило применяется ко входящим соединениям и гарантирует, что любой клиент, обращающийся к сервису, пройдёт через взаимную аутентификацию.
Определив политику обработки входящих запросов внутри сервисной сети, вернёмся к тестовому запросу:
sh-4.4$ curl -i -X POST curator:8080 -d '{"message": "Test Kintsugi"}'
HTTP/1.1 503 Service Unavailable
content-length: 95
content-type: text/plain
server: envoy
upstream connect error or disconnect/reset before headers. reset reason: connection termination
В ответе на запрос получаем ошибку с HTTP-кодом 503 Service Unavailable
, свидетельствующую о недоступности сервиса. Почему так произошло? Всё дело в том, что теперь в сервисной сети действует правило, требующее от сервиса-клиента включённого режима mTLS при установлении соединения. Устраним данный недочёт. В этом нам поможет ресурс DestinationRule
, определяющий правила исходящего соединения:
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: enable-mtls-dr
spec:
exportTo:
- .
host: '*.svc.cluster.local'
trafficPolicy:
tls:
mode: MUTUAL_TLS
DestinationRule
предоставляет широкий спектр возможностей для управления исходящим трафиком. Остановимся на наиболее важных для нас параметрах: spec.host
— адрес хоста, к которому будут применены политики, описанные в ресурсе, и spec.trafficPolicy.tls.mode
— режим TLS соединения. Istio предполагает использование следующих режимов:
DISABLE
: отключение TLS, все подключения между сервисами устанавливаются в незащищённом режиме, нет шифрования (данные передаются в открытом виде);SIMPLE
: используется, когда нужна защита передаваемых данных, но проверка подлинности клиента не требуется;MUTUAL
: режим активирует взаимную аутентификацию, проверяется подлинность обоих респондентов — это наиболее безопасный вариант установления соединения;ISTIO_MUTUAL
: функционально соответствует режиму MUTUAL, при его использовании не требуется указывать данные сертификатов и ключа клиента — всю работу по генерации и управлению сертификатами берёт на себя Istio.
В нашем примере мы создали ресурс DestinationRule
, в котором политики применяются ко всем сервисам в домене *.svc.cluster.local
и устанавливают режим работы ISTIO_MUTUAL
.
Если в глобальной конфигурации сервисной сети установлен флаг enableAutoMtls: true
, то явное указание параметров trafficPolicy.tls
в ресурсе DestinationRule
становится необязательным: Istio автоматически применяет mTLS для взаимодействия между сервисами. Однако определение trafficPolicy.tls
в DestinationRule
остаётся важным инструментом для полного контроля над шифрованием трафика и политиками безопасности, независимо от глобальных настроек сервисной сети.
После конфигурирования повторим запрос к испытуемому сервису и выполним дамп сетевого трафика:
sh-4.4$ tcpdump -A -i eth0
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
sh-4.4$ curl -i -X POST curator:8080 -d '{"message": "Test Kintsugi"}'
HTTP/1.1 200 OK
server: envoy
content-length: 22
content-type: application/json
Запрос выполнен успешно с ожидаемым результатом. Взглянем на дамп трафика:

На скриншоте видим запрос от сервиса-клиента к прикладному сервису curator
и передаваемые в канале данные в зашифрованном виде. Всё работает!
Ресурсы PeerAuthentication
и DestinationRule
позволяют гибко конфигурировать политики аутентификации в среде Istio. Однако важно учитывать, что неправильная настройка политик может приводить к рассогласованию настроек транспорта и, как следствие, прерыванию связи между сервисами и дестабилизации работы вашего приложения.
Мы рассмотрели сценарий межсервисной аутентификации внутри сервисной сети. Но как обеспечить безопасность приложения в случае, если сервис или пользователь находится за её пределами, и механизму Istio уже неподвластен полный контроль и управление объектами аутентификации? Здесь нам на помощь приходят специальные возможности для идентификации и проверки подлинности клиента.
Аутентификация запросов с использованием jwt
Аутентификация JSON Web Token (JWT) — это один из популярных методов, который широко применяется в современных веб-приложениях и API. Основные компоненты JWT-аутентификации:
Issuer: служба, выпускающая токены;
JWKS (JSON Web Key Set): набор ключей, используемых для подписи и проверки подписей токена;
Token: сам токен, который содержит информацию о пользователе и данные, используемые для проверки подлинности.
Процесс аутентификации состоит из следующих этапов:
Выпуск токена: пользователь предоставляет свои учётные данные (имя пользователя, пароль) серверу аутентификации. Если проверка прошла успешно, то сервер возвращает JWT-токен.
Передача токена: токен отправляется вместе с каждым HTTP-запросом в заголовке
Authorization
с префиксомBearer
. Например:curl -H “Authorization: Bearer <token>” https://kintsugi.solution.test
.-
Проверка токена: когда запрос поступает на ресурс, защищённый Istio, происходит проверка токена. Она включает в себя следующие шаги:
проверка подписи токена с использованием JWKS;
проверка срока действия токена;
проверка соответствия требуемым данным: идентификатор пользователя, роли и другие атрибуты.
Перед тем как перейти к настраиванию инфраструктуры дополним нашу архитектурную схему компонентом IdP (Indentity Provider). Это сервис управления идентификацией пользователя, используемый для аутентификации, авторизации пользователей в прикладном сервисе. Также он отвечает за выпуск JWT-токенов.

Для контроля изменений поведения в процессе дальнейшего конфигурирования выполним исходный запрос к сервису curator
снаружи сервисной сети с использованием ранее настроенного ресурса Ingress
:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
В нашем запросе к API-ресурсу readiness
компонента curator
используем метод GET
. Работаем по протоколу HTTP 1.1
, обрабатываем все редиректы и обеспечиваем клиента сертификатами доверенных удостоверяющих центров, необходимых для проверки сертификата сервера при установлении защищённого соединения. В результате получаем успешный ответ с кодом 200 OK
, что сигнализирует нам о беспрепятственном доступе к сервису извне:
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
Для настройки политик аутентификации в Isito воспользуемся ресурсом RequestAuthentication
, где определим правила аутентификации для входящего трафика, адресованного прикладному сервису. Это позволит нам проконтролировать то, какие запросы будут разрешены, а какие — отклонены на основе предъявляемого JWT-токена:
---
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: kintsugi
spec:
selector:
matchLabels:
app: curator
jwtRules:
- forwardOriginalToken: true
issuer: https://idp.solution.test/auth/realms/da-dp01-db-kintsugi-master
jwksUri: https://idp.solution.test/auth/realms/da-dp01-db-kintsugi-master/protocol/openid-connect/certs
Ключевые параметры ресурса:
spec.jwtRules.issuer
: идентификатор службы, выпустившей токен;spec.jwtRules.jwksUri
: ресурс для получения публичных ключей для проверки подписи JWT-токенов;spec.selector.matchLabels
: метка, определяющая, к каким объектам будет применена политика аутентификации;spec.jwtRules.forwardOriginalToken
: определяет, будет ли перенаправлен токен в запросе приложению.
Ресурс устанавливает правило аутентификации входящего трафика для сервиса с меткой app: curator
. Когда запрос поступает в сервисную сеть, Istio проверяет наличие валидного JWT-токена, подписанного издателем issuer
и проверенного через указанный JWKS URI.
Проверим, как отработает запрос, содержащий невалидный токен в заголовке Authorization
:
sh-4.4$ INVALID_TOKEN="This is invalid token"
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt -H "Authorization: Bearer $INVALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="https://curator-da-dp01-db-kintsugi-master.solution.test/readiness", error="invalid_token"
content-length: 79
content-type: text/plain
date: Fri, 16 May 2025 21:04:58 GMT
server: istio-envoy
Получаем ответ с HTTP-кодом 401 Unauthorized
, и лог Ingress Gateway
подтверждает это:
[2025-05-16T21:04:58.978Z] "GET /readiness HTTP/1.1" 401 - jwt_authn_access_denied{Jwt_is_not_in_the_form_of_Header.Payload.Signature_with_two_dots_and_3_sections} - "-" 0 79 4 - "172.21.7.100" "curl/7.61.1" "9e820f4e-dfe6-4cf4-9d46-03cc23b2878d" "curator-da-dp01-db-kintsugi-master.solution.test" "-" outbound|8080||curator.da-dp01-db-kintsugi-master.svc.cluster.local - 172.21.14.182:8443 172.21.7.100:52562 curator-da-dp01-db-kintsugi-master.solution.test - 3371
Повторим наш запрос. На этот раз передадим токен в корректном формате, но он будет выпущен сторонней службой, недоверенной в нашем домене Istio. Пример распарсенного токена, который будем использовать в запросе:
{
"alg": "RS256",
"typ": "JWT",
}
{
"exp": 1747419301,
"iat": 1747419001,
"jti": "f3fe8ea9-075b-4cb5-b729-20842373fd60",
"iss": "https://invalid.solution.test/auth/realms/da-dp01-db-kintsugi-master",
"aud": "kintsugi",
"sub": "d9ea00d0-0803-42b3-a1ed-fb277159308b",
"name": "user",
"preferred_username": "kintsugi"
}
Здесь обратим внимание, что значение ключа iss
указывает на ложный провайдер аутентификации и отличается от указанного в политике аутентификации RequestAuthentication
в качестве значения ключа issuer
.
Запрос:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt -H "Authorization: Bearer $INVALID_ISSUER_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="https://curator-da-dp01-db-kintsugi-master.solution.test/readiness", error="invalid_token"
content-length: 28
content-type: text/plain
date: Fri, 16 May 2025 21:10:47 GMT
server: istio-envoy
В логе Ingress Gateway
видим специфичную ошибку, сигнализирующую о том, что указанный в токене issuer
не сконфигурирован в нашей политике и, как следствие, не является доверенным. В результате чего получаем отказ в аутентификации запроса:
[2025-05-16T21:10:48.189Z] "GET /readiness HTTP/1.1" 401 - jwt_authn_access_denied{Jwt_issuer_is_not_configured} - "-" 0 28 7 - "172.21.7.100" "curl/7.61.1" "4418cfe3-d23b-4f28-976f-015c4d74ed66" "curator-da-dp01-db-kintsugi-master.solution.test" "-" outbound|8080||curator.da-dp01-db-kintsugi-master.svc.cluster.local - 172.21.14.182:8443 172.21.7.100:50460 curator-da-dp01-db-kintsugi-master.solution.test - 3376
Наконец, выполним проверку аутентификации с корректным токеном, выпущенным доверенным провайдером идентификации, и убедимся, что аутентификация пройдёт успешно. Пример распарсенного токена, который будем использовать в запросе:
{
"alg": "RS256",
"typ": "JWT",
}
{
"exp": 1747419665,
"iat": 1747419365,
"jti": "94360a03-96cb-4ed6-a483-dee8defeb89e",
"iss": "https://idp.solution.test/auth/realms/da-dp01-db-kintsugi-master",
"aud": "kintsugi",
"sub": "daf933df-11b5-40b9-9d23-8ee034595123",
"name": "user",
"preferred_username": "kintsugi"
}
Приведённый токен действителен и соответствует требованиям правил jwtRules
, определённым в политике аутентификации RequestAuthentication
. Поэтому перейдём к запросу:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
date: Fri, 16 May 2025 21:16:28 GMT
server: istio-envoy
content-length: 0
Запрос успешно прошёл проверку токена, по результатам чего мы получили ответ от нашего прикладного сервиса, об этом свидетельствует лог Ingress Gateway
:
[2025-05-16T21:16:21.703Z] "GET /readiness HTTP/1.1" 200 - via_upstream - "-" 0 0 37 25 "172.21.9.138" "curl/7.61.1" "ace6d46b-2863-47ca-af42-1c3ff15e3ac9" "curator-da-dp01-db-kintsugi-master.solution.test" "172.21.13.146:8080" outbound|8080||curator.da-dp01-db-kintsugi-master.svc.cluster.local 172.21.14.182:50934 172.21.14.182:8443 172.21.9.138:12858 curator-da-dp01-db-kintsugi-master.solution.test - 3381
Итак, мы увидели, как работает механизм аутентификации в Istio. Чуть позже посмотрим, что можно было бы здесь улучшить. А пока вернёмся к вопросу взаимной сетевой аутентификации с использованием протокола mTLS и немного подзакрутим гайки в части взаимодействия внешнего пользователя или сервиса с нашим приложением.
Ранее с помощью Gateway мы настроили взаимодействие с прокси с использованием режима установления защищённого соединения mode: SIMPLE
, когда клиент проверяет подлинность сервера, в роли которого выступает наш прикладной сервис curator
. Почему бы дополнительно не убедиться в подлинности клиента, будь то пользователь или внешний клиентский сервис?
Доработаем конфигурацию TLS-ресурса Gateway для включения режима взаимной аутентификации. Установим значение параметра spec.servers.tls.mode
в режим MUTUAL
:
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
labels:
app: curator
name: curator-gw
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- curator-da-dp01-db-kintsugi-master.solution.test
port:
name: https
number: 8443
protocol: HTTPS
tls:
caCertificates: /secrets/istio/ingressgateway-ca-certs/ca.crt
mode: MUTUAL
privateKey: /secrets/istio/ingressgateway-certs/tls.key
serverCertificate: /secrets/istio/ingressgateway-certs/tls.crt
Проверим, как изменилось поведение при установлении защищённого соединения с использованием взаимной аутентификации mTLS:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
curl: (56) OpenSSL SSL_read: error:1409445C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required, errno 0
При попытке выполнить запрос получаем ошибку. Нам указывают на отсутствие в запросе информации о клиентском сертификате, по этой причине получаем отказ.
Исправим ситуацию и повторим запрос уже с предъявлением выпущенного сертификата и ключа клиента:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
Запрос завершился успешно, по результатам взаимной аутентификации всё работает. Мы получили полноценное работающее решение с использованием взаимной аутентификации mTLS клиента и сервера.
Цель достигнута. Кажется, что процесс аутентификации доведён до совершенства. На этом можно было бы остановиться, но…
Смоделируем ситуацию, когда нелегитимный клиент пытается обратиться к нашему сервису, и у него есть действующий сертификат пользователя. Гипотетически такая ситуация может возникнуть, например, при блокировке пользователя и несвоевременном отзыве его персонального сертификата.
Предварительно сгенерировав сертификат и ключ, выполним тестовый запрос к сервису:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert illegitimate.crt --key illegitimate.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
Как мы видим, запрос завершился успехом, потому что в данном случае по-другому не могло и быть. Самое время задуматься о механизме фильтрации подключений по особому признаку. Например, по атрибутам сертификата клиента. Ранее мы упоминали о ресурсе EnvoyFilter
, его основное предназначение в Istio — тонкое конфигурирование Envoy-прокси. Как раз-таки здесь он нам и поможет. Сконфигурируем фильтр так, чтобы при обработке запроса производился анализ заголовка, содержащего сертификат клиента, и обработка выполнялась только для определённого значения атрибута Common Name (CN):
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: ingressgateway-cert-filter
labels:
app.kubernetes.io/managed-by: Helm
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
portNumber: 8443
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.rbac
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
rules:
action: ALLOW
policies:
auth-by-cert-policy:
permissions:
- any: true
principals:
- or_ids:
ids:
- header:
name: x-forwarded-client-cert
safe_regex_match:
google_re2:
max_program_size: 10000
regex: .*Subject="CN=kintsugi".*
workloadSelector:
labels:
istio: ingressgateway
Определяем патч, который будет применён к конфигурации Envoy. Патч применяется в контексте Gateway
для порта 8443
и выполняет разрешающее действие по результатам проверки содержимого заголовка x-forwarded-client-cert
, в котором передаются данные клиентского сертификата. Фильтр применяется к компоненту ingressgateway
. Пример фильтра работает по принципу белого списка и допускает обращение к ресурсам прикладного сервиса клиента с сертификатом, содержащим атрибут CN
со строго определённым значением: CN=kintsugi
Проверим исправность работы нашего механизма фильтрации запросов. Для этого выполним запрос с клиентским сертификатом, где CN=illegitimate
:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert illegitimate.crt --key illegitimate.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 403 Forbidden
content-length: 19
content-type: text/plain
server: istio-envoy
Повторим запрос для клиента с сертификатом, где CN=kintsugi
:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
Приведённые примеры демонстрируют простейший кейс использования механизма фильтрации запросов. Применяя всю мощь регулярных выражений, Istio позволяет сформировать гибкую политику обработки трафика, которую можно использовать в своём проекте.
Итак, мы рассмотрели возможные сценарии использования механизма аутентификации в Istio. Разобрали примеры конфигурации и, наконец, подошли к не менее важному механизму обеспечения безопасности в нашем защищённом контуре — авторизации.
Авторизация
Авторизация — механизм определения прав доступа для уже аутентифицированного пользователя или сервиса. Базовое управление политиками авторизации в Istio выполняется с использованием ресурса AuthorizationPolicy
.
Рассмотрим процесс авторизации в действии. Вернёмся к примеру запроса с использованием JWT-токена, с ним мы тестировали механизм аутентификации:
sh-4.4$ INVALID_TOKEN="This is invalid token"
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt -H "Authorization: Bearer $INVALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 401 Unauthorized
В примере мы выполнили запрос с указанием невалидного токена. и даже получили ответ с HTTP-кодом 401 Unauthorized
. Но всё ли было учтено? Чтобы ответить на этот вопрос, повторно выполним запрос, акцентируя внимание на некоторых деталях:
Заблаговременно обогатим наш запрос данными о клиентском сертификате и ключе, чтобы избежать проблемы с проверкой подлинности клиента.
В исследовательских целях исключим из запроса заголовок авторизации:
Authorization: Bearer $INVALID_TOKEN
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
В результате получаем ответ с HTTP-кодом 200 OK
, что означает успешно выполненный запрос в обход аутентификации клиента. Это явно не входило в наши планы. Теперь наша задача — обеспечить контроль подобных запросов. И в качестве первого рубежа обороны нам надо реализовать проверку наличия токена в запросе. Для этого воспользуемся возможностью политики авторизации и реализуем правило проверки нашего запроса:
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: authz
spec:
action: ALLOW
rules:
- from:
- source:
requestPrincipals:
- '*'
selector:
matchLabels:
app: curator
Ключевые параметры конфигурации AuthorizationPolicy
:
-
spec.action
: действие, которое должно применяться в случае выполнения правил, описанных в политике:ALLOW
: разрешить доступ;DENY
: отказать в доступе.
spec.rules
: набор правил, определяющих условия, при которых действие выполняется; Каждый элемент массиваrules
представляет собой отдельное правило.spec.rules.from.source.requestPrincipals
: перечень источников запросов. Это может быть список пользователей или групп.spec.selector
: селектор для выбора сервисов, к которым будет применяться политика.
Конфигурация ресурса создаёт политику авторизации с именем authz
, которая будет применена для прикладного сервиса curator
. Политика разрешает запросы, в которых предоставляется информация о токене, и не накладывает ограничений на идентификатор источника запроса.
Применив политику, повторим запрос к нашему сервису без передачи заголовка авторизации с токеном:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 403 Forbidden
content-length: 19
content-type: text/plain
server: istio-envoy
Получаем ответ с HTTP-кодом 403 Forbidden
— в нашем случае ожидаемый результат. Дополнительно проверим нашу политику на предмет корректной работы и отправим запрос с валидным токеном, выпущенным доверенным провайдером идентификации:
curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
Таким образом, мы проверили правильность работы сконфигурированной политики и реализовали проверку обязательного наличия JWT-токена в запросе.
Из описанного выше примера можно заметить, что политики авторизации дают возможность анализировать содержимое объектов токена (claims) и на основе этого выполнять разрешающее или запрещающее действие.
Перейдём к следующему кейсу и переработаем созданное правило. Сделаем его более строгим. На примере запроса к сервису Kintsugi реализуем политику авторизации. Её задача будет заключаться в анализе содержимого объекта roles
токена, в котором определены роли, назначенные клиенту.
Для демонстрации работы правила допустим, что у нас есть роль kintsugi-readiness-role
, позволяющая обращаться к ресурсу readiness
приложения, и определять его готовность к обработке запросов.
{
"alg": "RS256",
"typ": "JWT",
}
{
"exp": 1747420820,
"iat": 1747420520,
"jti": "89858c3b-f955-4b9a-8430-e76444e68d71",
"iss": "https://idp.solution.test/auth/realms/da-dp01-db-kintsugi-master",
"aud": "kintsugi",
"sub": "daf933df-11b5-40b9-9d23-8ee034595123",
"name": "user",
"realm_access": {
"roles": [
"readiness-role"
]
},
"preferred_username": "kintsugi"
}
С помощью политики авторизации определим политику доступа к заданному API-ресурсу приложения и посмотрим, как это работает:
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: authz
spec:
action: ALLOW
rules:
- to:
- operation:
methods:
- GET
paths:
- /readiness
from:
- source:
requestPrincipals:
- '*'
when:
- key: 'request.auth.claims[realm_access][roles]'
values:
- kintsugi-readiness-role
selector:
matchLabels:
app: curator
Политика авторизации применяет разрешающее правило для выполнения API-запроса к ресурсу readiness
компонента curator
при наличии у объекта аутентификации необходимой роли kintsugi-readiness-role
. Вместе с этим установленное разрешающее правило ставит неявный запрет на обращение к ресурсам API, не определённым в политике.
Выполним запрос к ресурсу liveness
:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/liveness
HTTP/1.1 403 Forbidden
content-length: 19
server: istio-envoy
Ресурс liveness
не определён в политике, поэтому мы получили отказ в доступе.
Убедимся в том, что для явно объявленного правила для ресурса readiness
запрос выполняется корректно:
sh-4.4$ curl --http1.1 -I -X GET -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 200 OK
server: istio-envoy
content-length: 0
Результаты выполнения запросов ожидаемы — политика работает корректно. Мы добавили возможность контроля доступа, но теперь уже на уровне ролевой модели приложения. Этот подход позволяет определять политики доступа пользователей к ресурсам приложения, исходя из содержимого JWT-токена, предлагая широкие возможности настраивания правил авторизации в среде Istio.
Финально взглянем, как работает ограничение используемого метода при выполнении запроса. Переквалифицируем наш запрос в POST
-запрос:
sh-4.4$ curl --http1.1 -I -X POST -L --cacert ca_bundle.crt --cert kintsugi.crt --key kintsugi.key -H "Authorization: Bearer $VALID_TOKEN" https://curator-da-dp01-db-kintsugi-master.solution.test/readiness
HTTP/1.1 403 Forbidden
content-length: 19
content-type: text/plain
server: istio-envoy
x-envoy-upstream-service-time: 4
Well done!
Политики авторизации Istio позволяют ограничить доступ к ресурсам приложения на основе анализа пользовательских атрибутов и проверке соответствующих правил, заданных в ресурсе AuthorizationPolicy
. Авторизация и аутентификация тесно взаимосвязаны и должны настраиваться комплексно. В примерах выше мы рассмотрели авторизацию на основе анализа содержимого JWT-токенов, но это лишь малая часть того, что умеет Istio.
Вместо заключения
Очень надеюсь, что приведённые мной изыскания окажутся вам полезными и пригодятся в проектировании и реализации микросервисных решений.
На основе примеров в статье я подготовил демо-проект, который можно использовать для тестирования кейсов и дальнейшего развития идеи построения защищённого микросервисного приложения: https://gitverse.ru/spbvalentine/istio-demo.
Больше о том, как мы делаем наши продукты, рассказываем в сообществе команды. Приходите, там же бывают наши вакансии и другие полезные активности.
Спасибо за внимание! Буду рад ответить на вопросы в комментариях.