
Привет! Меня зовут Александр, я ведущий разработчик VK Tech в команде, которая занимается сервисом Kubernetes в нашем публичном облаке. Все чаще провайдеры отказываются от модели, где пользователь получает полный контроль над кластером и всеми управляющими компонентами, в пользу управляемых (managed) решений. В такой архитектуре вы остаетесь администратором внутри своего кластера — создаете неймспейсы, деплоите приложения, настраиваете RBAC, — но инфраструктура, control plane и системные компоненты полностью управляются провайдером и скрыты от глаз пользователя.
В начале декабря наша команда выпустила новый сервис managed-k8s, в котором как раз реализован такой подход. Я расскажу вам про то, как выглядит наш Kubernetes с точки зрения архитектуры и каким образом Gatekeeper делает архитектуру безопасной.
Суть модели
Чтобы понять модель managed и то, какую проблему решает Gatekeeper, представьте жилой дом:
Вам выдают ключи только от вашей квартиры.
Подвал и чердак контролирует управляющая компания.
Охрану же обеспечивает ЧОП — охранник стоит у входа, проверяет всех и не пускает нарушителей.

Жители дома получают полный контроль над своей квартирой, управляющая компания заботится обо всех компонентах внутри дома, а ЧОП следит, чтобы пользователь не выходил за рамки дозволенного.
Чтобы реализовать такую модель, мы внедрили концепцию Kubernetes-in-Kubernetes, куб в кубе. Это означает, что вместо размещения всех компонентов в одном общем кластере мы выделяем отдельные изолированные окружения: один для наших системных сервисов, а другой — для пользовательской рабочей нагрузки.
Как устроен куб в кубе
Сначала разворачиваются основные компоненты, такие как kube-apiserver, kube-scheduler, kube-controller-manager, etcd, kubelet. Это база, на которой строится дальнейшее решение.
Этих компонентов уже достаточно, чтобы можно было деплоить контейнеры и управлять кластером:

Далее в существующий кластер мы деплоим еще один такой набор компонентов, которые тоже образуют под собой готовый k8s-кластер. Его мы и отдаем пользователю:

При такой архитектуре пользователь видит у себя только рабочие узлы, где он будет запускать свою нагрузку, и поды, которые там работают. Доступ до элементов control-plane у него отсутствует:

Обеспечение безопасности
Чтобы можно было безопасно работать в таком кластере, действия пользователя все-таки нужно ограничивать. У него не должно быть какой-либо возможности повысить свои привилегии и получить доступ до управляющих компонентов. Как минимум нужно запретить монтирование директорий с хоста внутрь контейнера и возможность получения доступа к процессам, которые запущены на хосте.
Для таких ограничений обычно используют Admission Controller’ы, которые валидируют запросы пользователя при работе с кластером k8s. В наших кластерах мы используем Gatekeeper, который решает проблему несанкционированных действий пользователя.
Но просто установить Gatekeeper в пользовательском окружении и отдать такой кластер в работу не получится. Рано или поздно пользователь может удалить наш Admission Controller, поскольку у него есть права администратора в своем окружении. Тем самым он повысит отказоустойчивость, и сделает свой кластер небезопасным в использовании. С нашей стороны мы должны были придумать, каким образом нужно установить Gatekeeper в кластер пользователя, чтобы пользователь:
не имел доступа к управляющим компонентам Gatekeeper;
не имел доступа к политикам Gatekeeper, не мог просматривать и изменять предустановленные политики Gatekeeper;
имел возможность установить свой какой-то Admission Controller, с которым он привык работать, без опасений, что его инструмент будет конфликтовать с нашим предустановленным Gatekeeper.

Чтобы решить такую задачу, для начала давайте посмотрим, из каких компонентов состоит Gatekeeper и как он работает.
Как устроен Gatekeeper
Посмотрите на схему:

Каждый раз, когда пользователь делает запрос на применение своих манифестов, kube-apiserver проверяет, есть ли зарегистрированные ValidatingWebhookConfiguration, чтобы перенаправить запрос на эти валидационные вебхуки.
Как правило, в конфигурации вебхуков указан сервис, на который нужно направить этот запрос:

Сервис, в свою очередь, знает, в какой под проксировать запрос на валидацию:

На схеме мы можем увидеть 2 деплоймента:
Контроллер-менеджер, который непосредственно проверяет входящие запросы.
Аудит, который проверяет, соответствуют ли объекты кластера заданным политикам или нет.
Также здесь видны необходимые ресурсы для работы Gatekeeper, секреты, сертификаты, сервисный аккаунт с необходимыми для его работы RBAC, CRD, сами политики и их шаблоны.
Gatekeeper в Kubernetes-in-Kubernetes

Наша задача состоит в том, чтобы разнести компоненты Gatekeeper по разным окружениям в кластере таким образом, чтобы пользователь не видел управляющих компонентов Gatekeeper и их зависимостей и не имел к ним доступа. При этом важно, чтобы валидация работала в пользовательском окружении.
Вернемся к нашей архитектуре и попробуем построить такое решение System k8s — системный кластер, куда у пользователя нет доступа и с помощью которого ставится User k8s, кластер, в котором пользователь запускает свою рабочую нагрузку.
Уберем лишние поды, чтобы они не отвлекали наше внимание, оставим etcd, чтобы понимать, где будут храниться наши созданные объекты, и kube-apiserver, с которым будет взаимодействовать пользователь при работе с кластером:

Пользователь делает запрос в kube-apiserver, который расположен в пользовательском пространстве(User-k8s). Вебхуки должны располагаться там же, чтобы стандартными средствами k8s-запрос перенаправлялся в вебхуки, прежде чем он сохранится в etcd, и сервис, на который эти вебхуки настроены. Поместим их туда — в пользовательское окружение:

У нас остаются основные компоненты Gatekeeper, gatekeper-audit, gatekeeper-controller-manager и все его зависимости — помещаем их в системный кластер (System k8s):

В итоге у нас получается следующая схема. Политики и сам контроллер с его зависимостям скрыт от влияния пользователя, валидация запросов осуществляется каждый раз, когда пользователь взаимодействует с kube-apiserver:

Но как же это все будет работать? Как компоненты в пользовательском окружении, узнают о компонентах в системном окружении, ведь их данные располагаются в разных etcd-хранилищах? Что нужно сделать, чтобы эта схема заработала?
Взаимодействие компонентов
Чтобы обеспечить такое взаимодействие, давайте уточним, что изображенные на схеме синим цветом контейнеры запускаются на мастер-ноде, которая имеет определенный IP-адрес:

Теперь давайте создадим в пользовательском окружении объект EndpointSlices и свяжем его с сервисом, который обслуживает валидационные вебхуки. В конфигурации EndpointSlices пропишем порт и IP-адрес мастер-ноды, на который по итогу будет перенаправлен запрос.
Далее в системном кластере создаем сервис с типом NodePort, который будет указывать на тот же порт, на который указывает EndpointSlice в пользовательском пространстве:


Далее мы делаем следующее: в системном кластере создаем сервис с типом NodePort, который будет указывать на тот же порт, на который указывает EndpointSlice в пользовательском пространстве:


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


Посмотрим, что у нас получилось:

Поды Gatekeeper запустились, находятся в состоянии Running — вроде бы все отлично. Но давайте заглянем в логи gatekeeper-controller-manager, чтобы убедиться, что мы не наблюдаем никаких ошибок:

Не все так здорово, как кажется на первый взгляд. В логах мы видим ошибки: Gatekeeper не может найти рядом с собой ValidatingWebhookConfiguration и MutatingWebhookConfiguration при запуске воркера, который следит за обновлением сертификата для вебхуков.
Исправляем ошибки
Чтобы избавиться от таких ошибок и не менять алгоритм работы Gatekeeper, вернемся к нашей схеме. Давайте создадим ValidatingWebhookConfiguration и MutatingWebhookConfiguration в системном окружении таким образом, чтобы запросы в системном кубе не валидировались Gatekeeper — ведь нам нужна валидация запросов только в пользовательском окружении. Для этого в манифесте, где мы описываем конфигурацию вебхука, укажем правила, по которым не будут проверяться изменения каких-либо ресурсов:


Таким образом, созданные политики Gatekeeper (они же constraint) никак не влияют на валидацию запросов в системном окружении — поскольку вебхуки, по сути, отключены. После таких манипуляций ошибки в логах контроллера Gatekeeper пропали.
Теперь давайте посмотрим, как работает собранная нами схема. Создадим политику, которая запрещает использовать монтирование директорий с хоста, и проверим ее:

Вот что мы увидим:

Проверим, что в пользовательском окружении эта политика отсутствует:

В пользовательском окружении нет никаких политик, и про ресурсы с типом contraint пользователю неизвестно. Попробуем запустить под и примонтировать в контейнер какой-нибудь каталог с хоста:

Применяем и получаем следующий результат:

Политики относительно namespace
Мы убедились, что наши политики работают, как мы этого и хотели. Давайте теперь проведем эксперимент: сделаем политику относительно какого-либо namespace:

Политику применили, теперь попробуем запустить под, который не нарушает правила политики и который после применения должен запуститься и работать. Запустим его в namespace test:

Пытаемся запустить под, который не нарушает политику, и получаем следующее сообщение:

Мы видим, что произошла какая-то ошибка, которая никак не связана с запрещающими действиями, которые мы установили в Gatekeeper. Видно, что есть какие-то проблемы в контроллере. Давайте посмотрим, что есть у нас в логах контроллера:

В логах видно, что контроллер ругается на то, что не может найти заданный namespace, и это верно, так как неймспейс есть в пользовательском окружении, но его нет в системном. Где же находится эта проверка? Вот в этом месте:

В запросе, который идет в kube-apiserver, контроллер проверяет, существует ли неймспейс, указанный в реквесте. Тут падает наша проверка, Gatekeeper прекращает валидацию, и поведение контроллера ломается. Похоже, что в этом месте нам нужно обойти проверку. Здесь понадобится сделать фича-флаг. Если Gatekeeper запущен с нашим фича-флагом, то проверку на существование namespace нужно пропустить. Так и поступим:

На схеме мы обновили контроллер-менеджер, выделив его другим цветом — в него внесли изменение на проверку существования неймспейса. Пользователь здесь имеет админский доступ в своем пользовательском окружении. По сути, он может избавиться от валидации Gatekeeper путем удаления вебхуков, сервиса и EndpointSlice — всего того, на чем держится валидация в пользовательском окружении:

После таких манипуляций валидация Gatekeeper перестанет работать. Какие у нас есть варианты:
Можно попробовать создать ValidationAdmissionPolicy, который запрещает удаление пользователем наших ресурсов (вебхуки, сервис и эндпойнт-слайс). Но пользователь имеет админский доступ, и рано или поздно он может найти это запрещающее правило, удалить его и удалить наши вебхуки.
Сделать ValidationAdmissionPolicy на проверку ValidationAdmissionPolicy, то есть запрещать удалять нашу созданную ValidationAdmissionPolicy. Это невозможно, так как просто не работает.
Отлавливать изменения вебхуков, когда кто-то удаляет их либо меняет их настройки. В Gatekeeper это недоступно.
Остается только один вариант. Патчим пользовательский kube-apiserver таким образом, чтобы при удалении или изменении наших ресурсов kubapi отбрасывал такой запрос. Мы выбрали помечать наши служебные ресурсы специальным набором лейблов, на которые будем опираться.
Вопрос с внесением каких-либо изменений в наши служебные ресурсы, которые находятся в окружении пользователя, решен.
Итоговая схема работы
Давайте посмотрим на итоговую схему, которая у нас получилась, со всеми дополнениями:

Нам пришлось:
Создать дополнительные вебхуки в системном окружении, которые, по сути, отключены. Они не проверяют изменения у ресурсов и, соответственно, пропускают все запросы.
Пропатчить Gatekeeper, чтобы он не проверял существование неймспейсов во входящем запросе. Спрятали эту фичу под флаг, запускаем Gatekeeper с этим флагом.
Внести изменения в kube-apiserver, чтобы обезопасить служебные ресурсы от влияния пользователя в его окружении. Мы спрятали их также под фича-флаг, с которым запускается наш kubeapi.
Вот теперь наша архитектура будет работать исправно и наш пользовательский кластер будет защищен от различных угроз, которые можно контролировать с помощью политик Gatekeeper. Также мы будем уверены в том, что пользователь никак не повлияет на работу нашего адмишен-контроллера.
Заключение
Давайте вернемся к началу доклада и вспомним про наш дом и ключи от нашей новой квартиры, которые мы получили.

ЧОП управляющей компании может не устраивать наших домовладельцев, его работу кто-то не сможет понять и простить. В реальной жизни люди могут влиять на решения управляющей компании и поменять ЧОП на тот, который их устраивает. То же самое можно сделать и в нашей архитектуре — и на место Gatekeeper установить ваш предпочитаемый Admission Controller. Это может быть как Kyverno, так и ваша собственная разработка. Возможно, вы тоже столкнетесь с какими-то подводными камнями, но у вас уже будет опыт, которым я с вами поделился. В дальнейшем он поможет вам в применении такой архитектуры:

А вот какие выводы можно сделать из полученного опыта деплоя Gatekeeper в архитектуру K8s-in-K8s в плане безопасности архитектуры:
Даже в managed-решениях пользовательский кластер требует ограничения прав пользователя — изоляция control plane не отменяет необходимости защиты data plane.
Admission-контроллеры должны быть совместимы с распределённой архитектурой: их компоненты могут находиться в изолированных средах, но обязаны надёжно взаимодействовать.
Системные компоненты в пользовательском кластере необходимо защищать от администратора-пользователя. Если защита невозможна — требуется патчинг kube-apiserver или, по возможности, вынос компонентов за пределы кластера.
На этом все, спасибо что прочли до конца, так же попробовать наши kubernetes-кластеры вы можете в нашем новом сервисе. А в комментариях делитесь своими мыслями и опытом по теме статьи.