
Привет, 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:
В результате внешний сервис становится доступен внутри mesh и может участвовать в маршрутизации и применении политик.
Определим сервис для проксирования трафика через Egress Gateway :
Через порт 5001 будет проходить трафик от всех сервисов к Egress Gateway .
Добавим точку входа на Egress Gateway :
Gateway принимает TCP-трафик на порту 5001 и передаёт его дальше в mesh. Для каждого канала создаётся отдельный subset в DestinationRule , который используется при выборе политики обработки трафика. Определим наборы политик , которые будем использовать для выбора TLS-конфигурации.
Для внутреннего трафика (от приложения к Egress Gateway ):
Для внешнего трафика (от Egress Gateway к серверу СУБД PostgreSQL):
Каждый subset соответствует отдельной TLS-политике и конкретному клиентскому сертификату.
Теперь настроим маршрутизацию. Попробуем разделить трафик на уровне sidecar с помощью sourceLabels и направить его в разные subsets :
Таким образом, на уровне sidecar мы разделяем трафик по sourceLabels и направляем его в разные subsets , каждому из которых соответствует своя TLS-конфигурация.
На первый взгляд схема выглядит корректной, поэтому перейдём к проверке и посмотрим, как она ведёт себя на практике.
Выполним подключение из сервиса psql-postgres :
Теперь выполним аналогичный запрос из сервиса psql-kintsugi :
Результат оказался не совсем тем, который мы ожидали.
Для сервиса psql-postgres подключение прошло успешно — соединение установлено, TLS используется, что подтверждается выводом pg_stat_ssl .
Однако при попытке подключения из сервиса psql-kintsugi получаем ошибку:
Обратим внимание на логи Egress Gateway :
Несмотря на то, что запросы приходят от разных сервисов, в обоих случаях используется один и тот же 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 :
Далее определим соответствующий Gateway , который будет принимать подключения на этом порту:
После этого внесём изменения в ранее созданный VirtualService postgres-vs-tls-origin-psql-kintsugi : вместо общего порта 5001(TCP) будем использовать выделенный порт 5002(TCP) , тем самым направляя трафик в отдельный транспортный канал.
Иными словами, мы явно разделяем трафик на уровне L4: каждому сервису соответствует собственный порт Egress Gateway , а значит — и собственная TLS-политика. В результате формируются независимые транспортные потоки, которые больше не смешиваются на уровне шлюза.
Как и в предыдущем сценарии, проверим подключение к базе данных для каждого сервиса. Для этого выполним соответствующую команду внутри пода каждого приложения.
Для сервиса psql-postgres подключение выполнено успешно, аналогично проверим для сервиса psql-kintsugi :
Результат ожидаемый: оба подключения устанавливаются успешно.
В логах Egress Gateway видно, как проксируется трафик и какие subset используются для маршрутизации:
Таким образом, нам удалось решить задачу многопользовательского подключения: каждый сервис устанавливает соединение с одной и той же СУБД, при этом на уровне Egress Gateway применяется корректная TLS-политика и используется соответствующий клиентский сертификат. Ключевая идея решения заключается в том, что контекст соединения кодируется в транспортных параметрах — в данном случае через порт. Это позволяет обойти ограничение TCP-маршрутизации в Service Mesh и обеспечить выбор политики на стороне инфраструктуры. Однако у такого подхода есть важная архитектурная особенность, которую необходимо учитывать при проектировании.
С ростом количества сервисов увеличивается число независимых транспортных каналов:
для каждого сервиса требуется выделенный порт на Egress Gateway ;
для каждого сервиса требуется выделенный порт на Egress Gateway ;
появляются дополнительные ресурсы, описывающие политики маршрутизации;
появляются дополнительные ресурсы, описывающие политики маршрутизации;
растёт общий объём конфигурации в Service Mesh.
растёт общий объём конфигурации в Service Mesh.
Это приводит не только к усложнению сопровождения, но и к потенциальному увеличению нагрузки на прокси (Envoy): возрастает количество listeners, clusters и правил маршрутизации, которые необходимо обрабатывать. В результате при масштабировании системы такой подход может стать фактором, влияющим на утилизацию ресурсов и управляемость конфигурации.
Тем не менее, данная схема остаётся рабочим и практичным решением для сценариев, где:
требуется централизованное управление TLS;
требуется централизованное управление TLS;
важно вынести работу с сертификатами из приложений;
важно вынести работу с сертификатами из приложений;
количество сервисов и соединений остаётся контролируемым.
количество сервисов и соединений остаётся контролируемым.
На практике такие сценарии редко остаются в изолированном виде. По мере роста системы могут потребоваться комбинированные подходы или поиск альтернативных механизмов маршрутизации и аутентификации — особенно когда количество сервисов и вариантов подключения начинает расти.
Тем не менее даже в рассмотренном варианте решение уже даёт ощутимый выигрыш: управление TLS централизуется, работа с сертификатами выносится на уровень инфраструктуры, а сами приложения избавляются от лишней сложности. В результате архитектура становится проще, а её сопровождение — предсказуемее.
Мы прошли путь от базового подключения через Service Mesh до более сложного многопользовательского сценария, разобрали ограничения TCP-маршрутизации и увидели, как они влияют на дизайн решения. Это хороший пример того, как инфраструктурные абстракции упрощают жизнь — но при этом требуют понимания своих границ.
По традиции прикладываю ссылку на репозиторий с примерами конфигурации, рассмотренными в статье: https://gitverse.ru/spbvalentine/istio-demo/tag/v1.2.0
Спасибо за внимание!