Салют! На связи команда платформы Авито, сегодня будем рассказывать про service mesh.

У Авито самописный service mesh — сначала это был Netramesh, который потом трансформировался в собственный control-plane и envoy в качестве data-plane. В начале этого года я добавлял в него поддержку mTLS, а сейчас мы успешно раскатываем это решение для межсервисного взаимодействия. 

Взаимная аутентификация делает сервисы безопаснее и помогает защитить передаваемые по сети данные. Такая фича уже есть в разных готовых решениях. Тем не менее, я узнал для себя ряд интересных моментов при создании своей реализации и теперь хотел бы поделиться этим опытом с вами. 

Подходы к аутентификации сервисов в разных решениях для service mesh

В зависимости от того, как устроен service mesh, будет отличаться и подход к авторизации сервисов. Например, если он построен на умных клиентах, то логику аутентификации нужно будет реализовывать в них. Это значит, что придётся подумать, как обеспечить ротацию и обновление секретов, которые используются для «подтверждения личности сервиса». 

Имеющиеся опенсорсные решения для service mesh обычно применяют другой подход — использование sidecar proxy рядом с каждым экземпляром сервиса. Ниже я проведу небольшой обзор того, как устроена аутентификация в распространённых решениях для service mesh.

Istio

Для межсервисной аутентификации в Istio предлагается один из двух способов: mutual TLS для аутентификации сервисов и использование JWT-токенов для аутентификации на уровне запросов. 

Первый способ предполагает заворачивание всего обмена между сервисами в TLS-туннель. Он устанавливается между двумя sidecar proxy и аутентифицирует клиентскую и серверную сторону по TLS-сертификатам. Сертификаты содержат идентификаторы сервисов, а управление ими берёт на себя control plane компонент istiod. Авторизация сервисов, т.е. правила, по которым определяется, какие сервисы могут друг с другом взаимодействовать, задаётся набором политик в CRD. Политики могут распространяться как на весь меш, так и на отдельные сервисы.

Для более простой и безопасной миграции Istio даёт возможность использовать PERMISSIVE режим. В нём разрешается использовать и TLS- и plaintext-трафик.

Если mutual TLS работает на уровне транспорта, то JWT-токены— на уровне запросов. Второй способ предполагает, что приложение должно будет получить JWT-токен от одного из провайдеров и использовать его на каждый запрос. Такой подход позволяет более гибко настраивать политики разграничения доступа на уровне конкретных эндпоинтов и методов.

Consul Service Mesh

Консул использует похожий подход как и в Istio для контроля доступа сервисов внутри меша — Intentions. Есть Identity-based Intentions, которые работают на уровне соединений (L4) и Application-Aware Intentions на уровне отдельных запросов (L7).

Подход Identity-based Intentions при установке соединений между сайдкарами использует тот же mTLS. Внутри сертификатов задаётся идентификатор сервисов, соответствующий спецификации SPIFFE X.509 Identity Document (о ней я ещё буду говорить дальше). Сертификаты управляются консулом, а хранятся в Vault, с котором у него есть встроенная интеграция.

Application-Aware Intentions позволяют  задать политики авторизации на основе параметров HTTP-запросов дополнительно к аутентификации на L4. Enterprise-версия Consul Service Mesh предлагает ещё более гибкие возможности авторизации.

Linkerd

Control plane Linkerd является Certificate Authority, который выпускает сертификаты, привязанные к сервис-аккаунту, и распространяет их до data-plane компонентов. Компоненты, в свою очередь, используют сертификаты для установки TLS-сессии и взаимной аутентификации сервисов. Политики авторизации задаются через CRD на уровне отдельных сервисов.

Open Service Mesh

Sandbox-проект CNCF. Здесь также используется envoy sidecar в качестве data-plane. Для аутентификации сервисов использует mTLS. Подписывать и выпускать сертификаты может отдельный компонент Tresor, возможна интеграция с Vault или cert-manager. Распространяет сертификаты control plane, используя протокол SDS.

Traefik Mesh

Достаточно легковесное решение по сравнению с Istio, которое использует несколько иной подход внедрения service mesh в работу сервисов. Разворачивается в виде даемонсета. Трафик сервисов заворачивается на него за счёт либо явных походов на сервисы через DNS имена вида <service>.<namespace>.traefik.mesh, либо настройки search в resolv.conf для всех контейнеров с сервисами. Так как межсервисный трафик проходит через proxy только со стороны клиента, обеспечить прозрачный для сервисов механизм аутентификации Traefik Mesh не может.

Для чего нам нужен mTLS в service mesh

У нас собственный service mesh, организованный по принципе sidecar proxy, и самописный control plane. Сейчас это нечто сложившееся исторически, про появление решения можно почитать тут. Я же расскажу про контекст, который мы имели на момент внедрения аутентификации.

Одна из задач, которую нужно было решить, — это запрет ходить в сервисы сторонним клиентам, условно говоря, просто из сети дата-центра. Контроль доступа на уровне отдельных методов, в целом, пока не требовался. Все запросы имеют заголовок X-Source, по которому эту логику можно реализовать в самих сервисах в случае необходимости.

Вторым важным для нас моментом была возможность использовать открытые сети в межсервисном обмене. Это позволяет  использовать вместо внутреннего облака публичные и не беспокоиться о том, насколько чувствительные данные передаются внутри.

То, что весь трафик сервисов завёрнут на sidecar proxy, позволяло организовать транспортную аутентификацию абсолютно прозрачно для сервисов. Как мы увидели из предыдущего раздела, практически единственным решением является использование mTLS между sidecar proxy при межсервисном обмене. Давайте вкратце посмотрим, что это такое.

Mutual TLS — это TLS соединение, в котором верифицируется не только серверный, но и клиентский сертификат. В процессе установления соединения стороны обмениваются сертификатами и публичными ключами, которые в них находятся. Далее каждая сторона может проверить целостность сертификата — ведь он передавался по открытой сети — за счёт проверки подписи на открытом ключе CA, которые эти сертификаты выпустил. Обычно, конечно, есть некая цепочка CA, вплоть до корневого CA.

После этого стороны убеждаются, что противоположная сторона имеет закрытый ключ, соответствующий его сертификату. Для этого стороны обмениваются сообщениями, зашифрованными открытыми ключами из сертификатов. Их корректная расшифровка подтверждает верификацию личностей каждой из сторон. После согласования параметров соединения и генерации сессионных ключей устанавливается зашифрованная TLS-сессия.

Таким образом, mTLS между сервисами решает сразу две проблемы: проблему аутентификации и проблему защиты передаваемых данных от прослушивания и подделки.

Межсервисная авторизация

Помимо аутентификации хотелось бы иметь авторизацию — разрешать ходить в сервис только тем его клиентам, которые должны в него ходить. Для этого принимающая соединения сторона должна не просто верифицировать сертификат, но и проверять, что зашитый в нём идентификатор сервиса относится к списку валидных клиентов. 

Envoy, который мы используем в качестве сайдкаров, позволяет это сделать через параметр match_subject_alt_names в блоке конфигурации TLS на листенере:

Чтобы сформировать эту конфигурацию, control plane должен знать обо всех потребителях сервиса и их идентификаторах. У нас это уже было реализовано — в тех CRD, которые деплоятся вместе с сервисом, все эти зависимости перечислены. Изначально в кластера их деплоит PaaS, который требует от каждого сервиса явного указания зависимостей в app.toml.

Бесшовная ротация секретов в Envoy

Для поддержки mTLS в service mesh важно, чтобы data plane компоненты могли автоматически и при этом бесшовно получать и обновлять сертификаты и ключи. Ротировать их лучше достаточно часто, чтобы снизить риск компрометации. В нашем случае срок жизни такого сертификата составляет часы. 

Для загрузки сертификатов envoy использует SDS, который является частью XDS-протокола. XDS — это стандартизированный API поверх GRPC, который envoy использует для обновления свой конфигурации. Поддержка SDS в control plane или в каком-либо другом компоненте означает, что envoy может запросить нужный ему сертификат и бесшовно обновить своё внутреннее состояние. 

В качестве SDS и инфраструктуры управления идентификаторами и сертификатами мы использовали Spire. Оказалось что его можно достаточно легко встроить в service mesh.

Что такое Spire

Spire — это эталонная реализация SPIFFE. SPIFFE — это стандарт, описывающий подход к идентификации и управлению секретами для инфраструктурных компонентов. SPIFFE и Spire, по сути, — ответ на проблемы масштабирования подходов к аутентификации компонентов в микросервисной среде.

Инсталляция spire cостоит из агентов, каждый из которых запускается на ноде кластера и сервера:

Сервер предоставляет API для администратора, который управляет набором идентификаторов сервиса (SPIFFE ID) и правилами их выдачи компонентам (Селекторы). Он же является CA для выпускаемых им сертификатов (SVID).

Агент запускается на ноде, регистрируется и подтверждает свою identity у spire-сервера. Он безопасно запрашивает у сервера SVID и хранит их у себя. Агент — это точка входа для sidecar proxy, которые запрашивают у него свои SVID. Он является SDS-сервером, который предоставляет доступ через UNIX-сокет. Почему через UNIX-сокет? Об этом я расскажу чуть ниже, сначала — про стандарт SPIFFE.

Что такое SPIFFE

SPIFFE — это набор спецификаций, решающий проблему идентификации и аутентификации приложений в динамичной микросервисной среде.

Спецификации SPIFFE описывают:

  • Как приложения идентифицируются. Для этого используется SPIFFE ID — структурированная в форме URI строка, которая уникально идентифицирует экземпляр приложения. Она состоит из имени trust domain и path.

  • Как SPIFFE ID кодируются в криптографически верифицируемый документ (SVID). Это может быть определённым образом сформированный сертификат или JWT-токен, которые содержат SPIFFE ID.

  • Как SVID выпускаются или перевыпускаются (Workload API): принципиально не требует никакой аутентификации со стороны приложения, идентифицируя его при помощи out-of-band механизмов.

Workload API стоит рассмотреть отдельно.а мой взгляд, он достаточно интересно решает проблему первоначального получения SVID тем компонентом, у которого пока нет никакого секрета, чтобы аутентифицировать себя.

Первичное получение SVID: проблема курицы и яйца

Представим такую ситуацию. 

Человек получает свой первый паспорт. Для этого ему нужно прийти в паспортный стол с фотографией и назвать свои данные или предъявить свидетельство о рождении. Будущий паспорт — первый документ человека с фотографией, поэтому ему нечего предъявить сотруднику паспортного стола, чтобы удостоверить свою личность. Теоретически, он может договориться с кем-то и поменяться своими данными. Тогда он получит паспорт со своей фотографией, но чужим именем, и обманет систему. 

Здесь и кроется проблема первичного получения идентифицирующего документа. В Spire её успешно решает Workload API. 

Выше я акцентировал внимание на том, что Workload API предоставляется в пределах машины через UNIX-сокет, вот как это работает:

  1. Приложение обращается к Workload API агента через UNIX-сокет, запрашивает SVID. В случае envoy это происходит по протоколу SDS.

  2. Агент через вызов getsockopt с опцией SO_PEERCRED получает PID, UID и GID клиентского процесса. В дальнейшем он использует их для получения дополнительной информации о процессе.

  3. Агент берёт PID процесса, получает имя контейнера через cgroup, идёт на /pods локального kubelet, получает спеку пода и использует её для формирования селекторов: имя пода, ноды, неймспейс, имя сервис-аккаунта и т.д.

  4. Затем агент отдаёт тот SVID, который наиболее точно матчится на набор селекторов, зарегистрированных заранее через Registration API сервера.

Селектор может выглядеть так: выдавать указанный spiffeID только процессу, запущенному в неймспейсе X.

В дальнейшем envoy использует этот SVID в качестве клиентского сертификата при установлении TLS-соединения до сервиса, в котором включён mTLS.

Eventual consistency конфигурации service mesh и переходные режимы работы mTLS

Конфигурация service-mesh является eventual-consistent системой. Для нас это значило то, что легко может возникнуть ситуация, когда принимающий запросы сервис ожидает подключения по TLS, а сервис-клиент ещё не получил актуальную конфигурацию и работает в обычном режиме. Это привело бы к потерянным запросам при включении mTLS, т.к. сервис-получатель ожидал бы TLS соединения от клиента, который его установить ещё не готов.

Для того, чтобы разрешить эту проблему мы ввели несколько режимов, через которые последовательно проходит сервис в момент включения mTLS.

В режиме INITIAL, сервис готов получать TLS запросы, но принимает запросы от всех клиентов по-прежнему в plain-тексте. После этого сервис переходит в режим PERMISSIVE, в котором все его клиенты отправляют запросы по TLS. При этом он по-прежнему готов принимать и plain-текст запросы. Такой переходный режим позволяет «дождаться» пока все envoy-сайдкары клиентских сервисов получат актуальные конфигурации от control-plan и перейдёт на использование TLS. Финальный режим STRICT отключает возможность принимать запросы без использования TLS.

Отключение mTLS приводит к таким же проблемам сходимости конфигурации, поэтому оно происходит также через переключение режимов, но в обратном порядке. Переход через эти режимы реализован внутри PaaS, прозрачно для конечных потребителей.

Подведём итоги

Существуют разные подходы к авторизации в разных service-mesh решениях. Условно их можно разделить на две категории — авторизация на уровне транспорта (mTLS) и авторизация на уровне запросов. На своём собственном service-mesh мы реализовали первое решение с помощью возможностей envoy по бесшовной ротации сертификатов/ключей (SDS) и  данных о связях между сервисами в нашем PaaS. А для решения проблемы управления и выдачи сертификатов использовали Spire — это интересный проект CNCF, который реализует спецификацию SPIFFE.

Если вам захотелось узнать о теме чуть больше — можете посмотреть запись доклада моего коллеги Алексея Егорова, он рассказало о нашем решении немного с другой точки зрения.

Комментарии (0)