Как мы строили безопасную микросервисную архитектуру с Service Mesh: интеграция с базами данных и масштабированиe

Как мы строили безопасную микросервисную архитектуру с Service Mesh: интеграция с базами данных и масштабированиe

Привет, 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

Спасибо за внимание!

← Cybersecurity