
Привет, Habr! Меня зовут Валентин, я DevOps-инженер команды Platform V Kintsugi. Мы занимаемся развитием облачного сервиса и на практике регулярно сталкиваемся как с архитектурными задачами построения распределённых систем, так и с вопросами обеспечения их безопасности.
В предыдущей части мы подробно разобрали механизм делегирования TLS-соединения на уровень Service Mesh и показали, как Egress Gateway может выступать полноценным участником PostgreSQL handshake. Однако этот сценарий рассматривался в упрощённой конфигурации — один сервис, один сертификат, одно подключение.

В реальной системе всё устроено иначе: разные сервисы используют разные учётные записи, разные сертификаты и обращаются к одним и тем же внешним системам. Это приводит к необходимости динамически выбирать TLS-политику и корректно маршрутизировать TCP-трафик в зависимости от источника. Именно здесь начинается самое интересное!
Представим сценарий, в котором несколько сервисов обращаются к одному серверу PostgreSQL, но при этом каждый сервис:
использует отдельную техническую учётную запись;
обладает собственным клиентским сертификатом;
работает со своей логической базой данных.
Разберём, как в таких условиях выстраивается взаимодействие и какие ограничения возникают при попытке централизовать управление безопасным подключением.

Смоделируем описанную ситуацию, развернув два сервиса — psql-postgres и psql-kintsugi. Каждый из них будет устанавливать защищённое соединение со своей базой данных, расположенной на сервере СУБД, при этом шифрование трафика будет выполняться на стороне Egress Gateway с использованием собственной технической учётной записи и соответствующего клиентского сертификата.
На этом этапе возникает ключевой вопрос: каким образом Egress Gateway должен определить, какой именно сертификат необходимо использовать для конкретного соединения? Предположим, что сервису psql-postgres необходимо аутентифицироваться на сервере базы данных с использованием учётной записи postgres (CN=postgres), а для сервиса psql-kintsugi создадим отдельную учётную запись kintsugi и выпустим соответствующий сертификат (CN=kintsugi). Попробуем решить эту задачу средствами Service Mesh, разделив трафик на уровне sidecar с использованием sourceLabels и subsets, и сопоставив каждому потоку свою TLS-политику.
Для этого последовательно опишем необходимые ресурсы. Регистрируем внешний PostgreSQL-сервер в реестре Istio:
--- apiVersion: networking.istio.io/v1beta1 kind: ServiceEntry metadata: name: postgres-se-tls-origin spec: endpoints: - address: 10.40.20.72 exportTo: - . hosts: - postgres.solution.test location: MESH_EXTERNAL ports: - name: postgres-5432 number: 5432 protocol: postgres resolution: STATIC
В результате внешний сервис становится доступен внутри mesh и может участвовать в маршрутизации и применении политик.
Определим сервис для проксирования трафика через Egress Gateway:
--- kind: Service apiVersion: v1 metadata: name: postgres-egress-service-tls-origin spec: ports: - name: tcp-5001 protocol: TCP port: 5001 targetPort: 5001 type: ClusterIP selector: app: egressgateway
Через порт 5001 будет проходить трафик от всех сервисов к Egress Gateway.
Добавим точку входа на Egress Gateway:
--- apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: postgres-gw-tls-origin-psql-postgres spec: selector: app: egressgateway servers: - hosts: - postgres.solution.test port: name: postgres-5001 number: 5001 protocol: TCP
Gateway принимает TCP-трафик на порту 5001 и передаёт его дальше в mesh. Для каждого канала создаётся отдельный subset в DestinationRule, который используется при выборе политики обработки трафика. Определим наборы политик , которые будем использовать для выбора TLS-конфигурации.
Для внутреннего трафика (от приложения к Egress Gateway):
--- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: postgres-internal-dr-tls-origin-postgres spec: exportTo: - . host: postgres-egress-service-tls-origin subsets: - name: postgres-internal-tls-origin-postgres
Для внешнего трафика (от Egress Gateway к серверу СУБД PostgreSQL):
--- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: postgres-external-dr-tls-origin spec: exportTo: - . host: postgres.solution.test subsets: - name: postgres-external-tls-origin-postgres trafficPolicy: tls: caCertificates: /secrets/istio/egressgateway-certs/ca.crt clientCertificate: /secrets/istio/egressgateway-certs/postgres.crt privateKey: /secrets/istio/egressgateway-certs/postgres.key mode: MUTUAL - name: postgres-external-tls-origin-kintsugi trafficPolicy: tls: caCertificates: /secrets/istio/egressgateway-certs/ca.crt clientCertificate: /secrets/istio/egressgateway-certs/kintsugi.crt privateKey: /secrets/istio/egressgateway-certs/kintsugi.key mode: MUTUAL workloadSelector: matchLabels: app: egressgateway
Каждый subset соответствует отдельной TLS-политике и конкретному клиентскому сертификату.
Теперь настроим маршрутизацию. Попробуем разделить трафик на уровне sidecar с помощью sourceLabels и направить его в разные subsets:
--- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: postgres-vs-tls-origin-psql-postgres spec: exportTo: - . gateways: - postgres-gw-tls-origin-psql-postgres - mesh hosts: - postgres.solution.test tcp: - match: - gateways: - mesh port: 5432 sourceLabels: app: psql-postgres route: - destination: host: postgres-egress-service-tls-origin port: number: 5001 subset: postgres-internal-tls-origin-postgres - match: - gateways: - postgres-gw-tls-origin-psql-postgres port: 5001 route: - destination: host: postgres.solution.test port: number: 5432 subset: postgres-external-tls-origin-postgres
Таким образом, на уровне sidecar мы разделяем трафик по sourceLabels и направляем его в разные subsets, каждому из которых соответствует своя TLS-конфигурация.
На первый взгляд схема выглядит корректной, поэтому перейдём к проверке и посмотрим, как она ведёт себя на практике.
Выполним подключение из сервиса psql-postgres:
10001@psql-postgres-8f7584f7d-j97dt:/$ psql "host=postgres.solution.test user=postgres port=5432 dbname=postgres sslmode=disable" postgres=# SELECT ssl, version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid(); ssl | version | cipher -----+---------+----------------------------- t | TLSv1.2 | ECDHE-RSA-AES256-GCM-SHA384 (1 row)
Теперь выполним аналогичный запрос из сервиса psql-kintsugi:
10001@psql-kintsugi-79db99ff5d-dghpz:/$ psql "host=postgres.solution.test user=kintsugi dbname=kintsugi port=5432 sslmode=disable" psql: error: connection to server at "postgres.solution.test" (10.40.20.72), port 5432 failed: FATAL: certificate authentication failed for user "kintsugi"
Результат оказался не совсем тем, который мы ожидали.
Для сервиса psql-postgres подключение прошло успешно — соединение установлено, TLS используется, что подтверждается выводом pg_stat_ssl.
Однако при попытке подключения из сервиса psql-kintsugi получаем ошибку:
FATAL: certificate authentication failed for user "kintsugi"
Обратим внимание на логи Egress Gateway:
[2026-04-26T21:28:47.690Z] "- - -" 0 - - - "-" 89 510 564 - "-" "-" "-" "-" "10.40.20.72:5432" outbound|5432|postgres-external-tls-origin-postgres|postgres.solution.test 172.21.15.211:38966 172.21.15.211:5001 172.21.14.161:53470 - - [2026-04-26T21:29:56.079Z] "- - -" 0 - - - "-" 84 108 14 - "-" "-" "-" "-" "10.40.20.72:5432" outbound|5432|postgres-external-tls-origin-postgres|postgres.solution.test 172.21.15.211:53216 172.21.15.211:5001 172.21.21.113:35306 - -
Несмотря на то, что запросы приходят от разных сервисов, в обоих случаях используется один и тот же subset — postgres-external-tls-origin-postgres. Следовательно, Egress Gateway всегда применяет одинаковую TLS-политику и один и тот же клиентский сертификат для всех соединений, независимо от источника. Именно поэтому подключение для пользователя kintsugi завершается ошибкой: сервер PostgreSQL получает сертификат с CN=postgres и отклоняет попытку аутентификации.
Почему так произошло?
На стороне sidecar-прокси действительно доступен контекст источника (включая sourceLabels), и именно поэтому мы можем успешно разделить трафик на этом этапе. Однако при проксировании через Egress Gateway формируется новый сегмент TCP-соединения, в котором исходный контекст маршрутизации уже недоступен, то есть метки сервиса, использованные при маршрутизации в sidecar, не попадают в следующий сегмент соединения. В результате на уровне Egress Gateway все входящие подключения выглядят как однородный поток TCP-трафика, и выбор TLS-политики становится невозможен без дополнительных механизмов. Именно поэтому, несмотря на корректную маршрутизацию на уровне sidecar, на Egress Gateway применяется одна и та же TLS-политика для всех подключений.
Возникает закономерный вопрос: если контекст не передаётся, можно ли зафиксировать его в самом соединении?
Вместо попытки передать контекст напрямую, мы можем явно закодировать его в L4-характеристиках трафика — например, направляя соединения от разных сервисов на разные внутренние порты Egress Gateway. Поскольку порт остаётся неизменным на всём пути следования пакета, Egress Gateway получает возможность различать потоки и применять к ним разные политики. В результате, мы переносим логику выбора с уровня абстракций Service Mesh на уровень транспортных параметров, доступных прокси.
Чтобы разделить трафик на уровне Egress Gateway, введём дополнительный транспортный признак — выделенный порт.
Сначала добавим новый порт в сервис, через который будет обрабатываться трафик от сервиса psql-kintsugi:
spec: ports: - name: tcp-5001 protocol: TCP port: 5001 targetPort: 5001 - name: tcp-5002 protocol: TCP port: 5002 targetPort: 5002
Далее определим соответствующий Gateway, который будет принимать подключения на этом порту:
--- apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: postgres-gw-tls-origin-psql-kintsugi spec: selector: app: egressgateway servers: - hosts: - postgres.solution.test port: name: postgres-tls-origin number: 5002 protocol: TCP
После этого внесём изменения в ранее созданный VirtualService postgres-vs-tls-origin-psql-kintsugi: вместо общего порта 5001(TCP) будем использовать выделенный порт 5002(TCP), тем самым направляя трафик в отдельный транспортный канал.
tcp: - match: - gateways: - mesh port: 5432 sourceLabels: app: psql-kintsugi route: - destination: host: postgres-egress-service-tls-origin port: number: 5002 subset: postgres-internal-tls-origin-kintsugi - match: - gateways: - postgres-gw-tls-origin-psql-kintsugi port: 5002 route: - destination: host: postgres.solution.test port: number: 5432 subset: postgres-external-tls-origin-kintsugi
Иными словами, мы явно разделяем трафик на уровне L4: каждому сервису соответствует собственный порт Egress Gateway, а значит — и собственная TLS-политика. В результате формируются независимые транспортные потоки, которые больше не смешиваются на уровне шлюза.
Как и в предыдущем сценарии, проверим подключение к базе данных для каждого сервиса. Для этого выполним соответствующую команду внутри пода каждого приложения.
10001@psql-postgres-8f7584f7d-j97dt:/$ psql "host=postgres.solution.test user=postgres port=5432 dbname=postgres sslmode=disable" postgres=# SELECT ssl, version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid(); ssl | version | cipher -----+---------+----------------------------- t | TLSv1.2 | ECDHE-RSA-AES256-GCM-SHA384 (1 row)
Для сервиса psql-postgres подключение выполнено успешно, аналогично проверим для сервиса psql-kintsugi:
10001@psql-kintsugi-79db99ff5d-dghpz:/$ psql "host=postgres.solution.test user=kintsugi port=5432 dbname=kintsugi sslmode=disable" kintsugi=> SELECT ssl, version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid(); ssl | version | cipher -----+---------+----------------------------- t | TLSv1.2 | ECDHE-RSA-AES256-GCM-SHA384 (1 row)
Результат ожидаемый: оба подключения устанавливаются успешно.
В логах Egress Gateway видно, как проксируется трафик и какие subset используются для маршрутизации:
[2026-04-26T21:41:45.957Z] "- - -" 0 - - - "-" 169 664 36540 - "-" "-" "-" "-" "10.40.20.72:5432" outbound|5432|postgres-external-tls-origin-postgres|postgres.solution.test 172.21.15.211:36170 172.21.15.211:5001 172.21.14.161:45982 - - [2026-04-26T21:45:06.743Z] "- - -" 0 - - - "-" 89 511 8024 - "-" "-" "-" "-" "10.40.20.72:5432" outbound|5432|postgres-external-tls-origin-kintsugi|postgres.solution.test 172.21.15.211:37830 172.21.15.211:5002 172.21.21.113:34314 - -
Таким образом, нам удалось решить задачу многопользовательского подключения: каждый сервис устанавливает соединение с одной и той же СУБД, при этом на уровне Egress Gateway применяется корректная TLS-политика и используется соответствующий клиентский сертификат. Ключевая идея решения заключается в том, что контекст соединения кодируется в транспортных параметрах — в данном случае через порт. Это позволяет обойти ограничение TCP-маршрутизации в Service Mesh и обеспечить выбор политики на стороне инфраструктуры. Однако у такого подхода есть важная архитектурная особенность, которую необходимо учитывать при проектировании.
С ростом количества сервисов увеличивается число независимых транспортных каналов:
для каждого сервиса требуется выделенный порт на
Egress Gateway;появляются дополнительные ресурсы, описывающие политики маршрутизации;
растёт общий объём конфигурации в Service Mesh.
Это приводит не только к усложнению сопровождения, но и к потенциальному увеличению нагрузки на прокси (Envoy): возрастает количество listeners, clusters и правил маршрутизации, которые необходимо обрабатывать. В результате при масштабировании системы такой подход может стать фактором, влияющим на утилизацию ресурсов и управляемость конфигурации.
Тем не менее, данная схема остаётся рабочим и практичным решением для сценариев, где:
требуется централизованное управление TLS;
важно вынести работу с сертификатами из приложений;
количество сервисов и соединений остаётся контролируемым.
На практике такие сценарии редко остаются в изолированном виде. По мере роста системы могут потребоваться комбинированные подходы или поиск альтернативных механизмов маршрутизации и аутентификации — особенно когда количество сервисов и вариантов подключения начинает расти.
Тем не менее даже в рассмотренном варианте решение уже даёт ощутимый выигрыш: управление TLS централизуется, работа с сертификатами выносится на уровень инфраструктуры, а сами приложения избавляются от лишней сложности. В результате архитектура становится проще, а её сопровождение — предсказуемее.
Мы прошли путь от базового подключения через Service Mesh до более сложного многопользовательского сценария, разобрали ограничения TCP-маршрутизации и увидели, как они влияют на дизайн решения. Это хороший пример того, как инфраструктурные абстракции упрощают жизнь — но при этом требуют понимания своих границ.
По традиции прикладываю ссылку на репозиторий с примерами конфигурации, рассмотренными в статье: https://gitverse.ru/spbvalentine/istio-demo/tag/v1.2.0
Спасибо за внимание!