Привет, Хабр!
Google, без преувеличения, изменил мир IT, подарив нам Kubernetes – систему, ставшую де-факто стандартом оркестрации контейнеров. И когда выбираешь управляемый Kubernetes от его же создателей, такой как Google Kubernetes Engine (GKE), ожидания, естественно, высоки. Уж кто-кто, а "первоисточник" должен уметь "готовить" свое детище идеально, предоставляя не только удобство, но и прозрачные, глубоко интегрированные и безопасные решения "из коробки". Особенно когда речь заходит о такой фундаментальной вещи, как сетевое взаимодействие и его безопасность.
GKE предлагает два режима работы кластеров: routes-based и VPC-native. Именно VPC-native кластеры позиционируются Google как обеспечивающие более тесную интеграцию с сетью VPC. Как утверждает Google, одно из преимуществ таких кластеров заключается в том, что IP-адреса подов (pods) нативно маршрутизируемы внутри сети VPC кластера и других сетей VPC, подключенных к ней через VPC Network Peering (подробнее см. документацию GKE по VPC-native кластерам). Это вселяет уверенность, что возможности VPC, включая мощный механизм GCP Firewall, будут доступны и для наших подов так же легко и нативно, как для обычных виртуальных машин.
Однако, погружаясь в детали настройки контроля сетевого доступа для подов к ресурсам внутри VPC, но внешним по отношению к самому Kubernetes (например, к базам данных Cloud SQL или другим бэкендам), начинаешь сталкиваться с нюансами. Нюансами, которые заставляют усомниться в "бесшовности" этой интеграции. Эта статья – не попытка принизить достижения Google или GKE. Скорее, это повод для всех нас, инженеров, задуматься о тех важных деталях реализации, которые часто остаются "под капотом". Повод погрузиться глубже, понять, как все устроено на самом деле, и какие компромиссы или сложности скрываются за маркетинговыми лозунгами. Ведь чем сложнее архитектура безопасности, тем выше вероятность ошибки конфигурации, особенно если ее компоненты и их взаимодействие не до конца понятны. Если даже у такого гиганта, как Google, в его флагманском продукте для Kubernetes есть подобные неочевидные моменты, то нам, инженерам, работающим с этими системами ежедневно, тем более важно понимать все тонкости для обеспечения надежности и безопасности наших собственных окружений.
Идеальный мир: Pod'ы и их права доступа на уровне VPC
Представьте: у вас есть Сервис А (набор подов), которому нужен доступ к вашей PostgreSQL базе данных (например, Cloud SQL for PostgreSQL), расположенной в том же VPC. И есть Сервис Б, которому такой доступ совершенно не нужен. В идеальном мире мы бы хотели определить политику минимального доступа на уровне нашего GCP Firewall, используя понятные идентификаторы подов. Например, "Разрешить трафик от подов из с определенной характеристикой (namespace, меткой, ассоциированной сервисной ролью), на которую можно сослаться в правиле GCP Firewall, на IP и порт БД. Запретить остальным." Просто, декларативно, эффективно.
Kubernetes NetworkPolicies: Защита внутри кластера (и на его границах)
GKE, конечно, предлагает Kubernetes NetworkPolicies. Это мощный инструмент для контроля трафика между подами внутри кластера и для контроля исходящего (egress) и входящего (ingress) трафика на уровне подов. В GKE, особенно при использовании Dataplane V2 (который основан на Cilium и использует eBPF), NetworkPolicies применяются на уровне узла (Node) и контролируют трафик до того, как он потенциально попадет под действие GCP Firewall для исходящего потока, или после того, как GCP Firewall пропустил входящий трафик на узел. Подробнее о них можно прочитать в официальной документации GKE по Network Policies.
NetworkPolicy скажет: "Подам с меткой app: service-a
разрешено инициировать соединение на DB_IP:DB_PORT
". Это важный и нужный слой защиты, позволяющий реализовать микросегментацию внутри кластера. Однако, сама по себе такая политика для Сервиса А лишь определяет, что его pod'ы могут попытаться отправить или принять трафик к указанной базе данных. И она не мешает разработчикам Сервиса Б, работающим в том же (или другом) namespace, создать свою NetworkPolicy, которая, например, не будет ограничивать исходящий трафик их подов.
Стоит отметить, что на практике команды разработки и администрирования далеко не всегда по умолчанию запрещают весь исходящий (egress) трафик для своих приложений. Зачастую, если приложение 'просто работает' и имеет доступ к нужным ему внутренним и внешним сервисам, мысль о строгом ограничении всех остальных исходящих соединений не приходит сама собой. Такой подход 'разрешено всё, что не запрещено явно' для исходящего трафика является довольно распространенным до тех пор, пока не появляется требование со стороны службы безопасности о внедрении принципа наименьших привилегий, вплоть до идей о полной сетевой изоляции (air-gapped environments). В отсутствие таких строгих требований от 'душного безопасника', многие системы продолжают работать с открытым egress, что переносит основную нагрузку по контролю доступа на ingress правила на стороне критичных ресурсов.
Важно понимать, что когда трафик от пода, разрешенный его NetworkPolicy, покидает пределы узла и направляется к ресурсу в VPC (например, к нашей базе данных), для GCP Firewall этот трафик будет выглядеть как исходящий от самого узла GKE. GCP Firewall на данном этапе не имеет информации о том, от какого конкретного пода на этом узле исходит данный сетевой пакет. Следовательно, любые правила GCP Firewall, предназначенные для контроля доступа подов к внешним ресурсам, должны будут оперировать идентификаторами узлов Kubernetes, а не идентификаторами самих подов.
Intranode Visibility и автоматически создаваемые правила GCP Firewall
Google постоянно развивает сетевые возможности GKE. С помощью функции "Intranode Visibility" даже трафик между подами на одном узле можно направить таким образом, что он пройдет через сетевой стек VPC. Это позволяет правилам GCP Firewall применяться к этому трафику.
Более того, GKE активно взаимодействует с GCP Firewall API для обеспечения базовой связности кластера, работы сервисов типа LoadBalancer
, Ingress
, Gateway
и для health checks. Документация по автоматически создаваемым GKE правилам файрвола подробно описывает множество таких правил. Например, правило gke-[cluster-name]-[cluster-hash]-all
с источником "Pod CIDR" и целью "Node tag" разрешает трафик между всеми подами в кластере.
Это демонстрирует, что GKE способен управлять правилами GCP Firewall. Однако, при внимательном изучении, все эти автоматически создаваемые правила, даже те, что касаются трафика от Pod CIDR, в качестве Target
(цели) используют Node tag
или IP-адреса LoadBalancer'ов. Это ключевой момент: даже когда GKE автоматизирует создание правил, он делает это, ориентируясь на идентификацию узла, а не отдельного пода, для применения политик GCP Firewall. Для исходящего же трафика от подов к произвольным ресурсам VPC (как наша база данных) GKE специфических разрешающих правил GCP Firewall не создает, полагаясь на правило VPC по умолчанию, разрешающее весь исходящий трафик, или на конфигурацию пользователя.
Таким образом, Intranode Visibility и автоматическое создание правил улучшают общую связность и управляемость, но не решают фундаментальную проблему идентификации конкретного пода-источника, на которую можно было бы сослаться в правиле GCP Firewall при доступе к внешней (по отношению к Kubernetes) базе данных.
Пример реализации: AWS EKS
Чтобы понять, какой функциональности нам может не хватать для более прямой интеграции, посмотрим на AWS EKS. Там для схожих задач существует функция "Security Groups for Pods", подробно описанная в документации AWS.
AWS VPC CNI Plugin может быть настроен для использования отдельных полноценных сетевых интерфейсов (Elastic Network Interfaces, ENIs) для подов или IP-адресов на дополнительных ENI узла. Эти ENIs являются "гражданами первого класса" в AWS VPC, и к ним могут применяться Security Groups.
С помощью специального Custom Resource Definition (CRD)
SecurityGroupPolicy
администратор кластера определяет, какие именно AWS Security Groups (VPC-level stateful firewall) должны быть применены к подам, выбранным по меткам (pod labels) или меткам их Kubernetes ServiceAccount.Таким образом, трафик от конкретных подов (или к ним) проходит через правила AWS Security Group, ассоциированной именно с этими подами. Файрвол VPC "понимает" группы подов и применяет к ним политики напрямую.
Это позволяет более прямо интегрировать политики безопасности уровня VPC с идентификацией подов, поскольку поды, по сути, получают собственные "network identity" в виде ENI (или через управление IP на ENI), на которые можно сослаться в правилах файрвола VPC.
Разумеется, это решение также имеет свою цену, как с точки зрения потребления IP-адресов и ENI, так и в плане некоторых операционных особенностей и ограничений. Детальное обсуждение этих аспектов выходит за рамки данной статьи, но важно понимать, что идеальных решений не существует, и каждый подход имеет свои компромиссы. Здесь EKS рассматривается лишь как пример иной архитектурной реализации для решения конкретной задачи.
GKE: Реальность 'VPC-Native' – или где же "network identity" пода для GCP Firewall?
А теперь вернемся в GKE. Как здесь обеспечить тот же уровень контроля со стороны GCP Firewall, чтобы он "узнавал" наши поды для доступа к внешней (по отношению к Kubernetes) базе данных?
GCP Firewall: Оперирует идентификаторами узлов GKE (VM-инстансов) – их сетевыми тегами (network tags) или сервисными аккаунтами (service accounts), ассоциированными с VM. Он "не знает" о подах, их метках Kubernetes или Kubernetes ServiceAccounts (даже если поды используют их через Workload Identity) для целей фильтрации их IP-трафика при обращении к внешним ресурсам.
IP-адреса подов в GKE: В режиме VPC-native поды получают IP-адреса из диапазона IP-алиасов на сетевом интерфейсе узла. Это эффективно с точки зрения использования IP-адресов и маршрутизации, но для GCP Firewall весь этот трафик по-прежнему исходит от узла (или его основного/алиас IP).
Отсутствие "сетевого интерфейса пода" в GCP VPC (в понимании AWS ENI): Ключевое отличие от модели AWS с ENI – в GCP нет концепции отдельного, управляемого на уровне VPC сетевого интерфейса, который бы GKE мог создать для каждого пода и к которому можно было бы применить отдельные сетевые теги GCP, на которые можно было бы сослаться в правилах GCP Firewall для дифференциации политик.
Основной тезис: Вся "native" интеграция GKE с VPC в контексте нашего сценария сводится к тому, что IP-адреса подов маршрутизируемы в VPC и к ним (с учетом Intranode Visibility) применяются общие правила GCP Firewall. Но если поды не имеют собственных "network identity" (вроде интерфейса с уникальными тегами GCP для каждого пода или группы подов), на которую можно сослаться в правилах GCP Firewall, то как GCP Firewall может применять к ним гранулярные правила для доступа к внешним ресурсам, не завязываясь на идентификацию всего узла?
Пример реализации: "Велосипед" на Node Pools
На практике, одним из возможных вариантов для достижения нужного поведения – чтобы GCP Firewall разрешил трафик от подов Сервиса А к базе данных, а от Сервиса Б – запретил – является следующий:
Поды Сервиса А должны быть размещены на узлах GKE, имеющих специальный GCP сетевой тег (например,
gke-node-allow-db-access-service-a
).В правилах GCP Firewall создается разрешение на доступ к БД для источников с тегом
gke-node-allow-db-access-service-a
.Все остальные поды, включая поды Сервиса Б, должны работать на узлах без этого тега (или с другими тегами, для которых доступ к этой БД запрещен).
Результат: Мы вынуждены создавать отдельные Node Pools для Сервиса А и Сервиса Б, если их требования к доступу через GCP Firewall к внешним ресурсам различаются. Один пул с тегом для доступа к БД, другой – без. Kubernetes NetworkPolicy внутри кластера все еще абсолютно необходима, чтобы разрешить подам Сервиса А вообще попытаться отправить этот трафик и для обеспечения микросегментации.
Последствия такого подхода:
Операционная сложность: Управление множеством специализированных Node Pool'ов, их тегами, правильным размещением подов (через node selectors/affinity) значительно усложняет эксплуатацию и увеличивает когнитивную нагрузку на команды.
Снижение эффективности использования ресурсов (bin-packing): Общие пулы узлов эффективнее утилизируются. Разделение на специализированные пулы может привести к недогрузке ресурсов, увеличению затрат и усложнению конфигурации Cluster Autoscaler.
Повышенный риск ошибок конфигурации: Чем больше движущихся частей (теги, селекторы, разные пулы, NetworkPolicies, GCP Firewall rules), тем выше вероятность человеческой ошибки, которая может привести к нежелательным доступам или, наоборот, к блокировке легитимного трафика. Даже Google в своей документации рекомендует: "Plan and design the configuration for your cluster... with your organization's Network administrators and Security engineers...", что косвенно подтверждает нетривиальность задачи.
Ограничения GCP: Существует лимит на количество сетевых тегов на один VM-инстанс. Для сложных систем с множеством сервисов, требующих гранулярного доступа к разным внешним системам, это может стать реальным архитектурным препятствием.
Усложнение аудита: Отследить, почему конкретный pod имеет (или не имеет) доступ к внешнему ресурсу, становится сложнее, так как нужно анализировать конфигурацию на нескольких уровнях: Kubernetes NetworkPolicy -> конфигурация деплоймента пода (node selector/affinity) -> Node Pool, на котором он запущен -> сетевые теги этого Node Pool -> правила GCP Firewall, использующие эти теги.
Проблема навешивания сетевых тегов GCP на поды – даже если бы это было возможно напрямую
Представим на секунду, что GKE позволил бы нам через аннотации пода напрямую указывать сетевые теги GCP, которые магическим образом применились бы к его трафику для GCP Firewall.
Бесконтрольное использование: Если любой разработчик, имеющий право создавать поды, сможет навешивать произвольные существующие сетевые теги GCP, это создаст огромную дыру в безопасности. Pod сможет "притвориться" любым другим VPC ресурсом или получить несанкционированный доступ, просто указав нужный тег. Это недопустимо.
Необходимость RBAC для тегов: Чтобы такой механизм был безопасным, потребовалась бы сложная система RBAC на уровне Kubernetes, контролирующая, какие команды/Kubernetes ServiceAccounts могут запрашивать какие GCP сетевые теги для своих подов. Это само по себе сложная задача, требующая продуманной модели управления.
Кто управляет жизненным циклом тегов GCP? Сетевые теги GCP – это ресурс уровня VPC, управляемый обычно сетевыми администраторами или через Infrastructure as Code (IaC) с соответствующими правами. Динамическое создание/применение тегов из Kubernetes должно быть строго регламентировано и интегрировано с общими процессами управления облачной инфраструктурой.
То есть, просто "разрешить аннотации для тегов пода" – не является панацеей без продуманной системы управления и безопасности. Нужен глубоко интегрированный, безопасный механизм, который сейчас, по сути, отсутствует для создания "network identity" пода, на которую можно было бы сослаться в правилах GCP Firewall при доступе к внешним ресурсам.
Так в чем же несоответствие маркетинга "VPC-Native" реальности для этого сценария?
Google активно продвигает GKE VPC-native кластеры. Как уже упоминалось, одно из ключевых преимуществ, согласно Google, – это нативная маршрутизация IP-адресов подов в сети VPC (см. документацию GKE по IP-алиасам). Это создает у пользователей определенные ожидания относительно глубины интеграции.
Однако, когда дело доходит до применения гранулярных правил GCP Firewall к трафику конкретных подов, направленному к внешним по отношению к Kubernetes ресурсам VPC:
Отсутствие "network identity" пода, на которую можно сослаться в GCP Firewall для внешних соединений: Pod не имеет собственного, идентифицируемого GCP Firewall интерфейса или сущности (вроде ENI в AWS, к которому можно применить отдельные теги/политики), к которой можно было бы применить GCP-теги или другие политики файрвола GCP напрямую для разделения доступа к внешним ресурсам. Трафик пода для GCP Firewall при обращении "наружу" (в пределах VPC, но к не-Kubernetes ресурсам) исходит от IP узла или его алиаса.
Workload Identity не решает эту сетевую задачу: Workload Identity подходит для предоставления IAM-прав подам на вызовы GCP API (например, к Cloud Storage, Pub/Sub, Cloud SQL Admin API). Это позволяет поду аутентифицироваться перед сервисами Google как специфический GCP Service Account. Однако, это не предоставляет механизм для GCP Firewall, чтобы идентифицировать IP-трафик конкретного пода и применить к нему правила на основе этого "принятого" (assumed) Service Account. Как указано в документации: "You cannot use Workload Identity Federation for GKE service accounts in the VPC firewall rules for source and target filtering." Вместо этого, GCP Firewall для правил на основе сервисных аккаунтов работает с сервисным аккаунтом, ассоциированным непосредственно с VM-инстансом (узлом). Таким образом, даже если pod аутентифицирован через Workload Identity, его сетевой трафик для GCP Firewall все равно будет исходить от узла и идентифицироваться по атрибутам узла.
Сложность и "велосипеды" вместо нативности: Пользователи вынуждены строить обходные пути, как, например, с Node Pool'ами, со сложной координацией между Kubernetes-конфигурациями и GCP-ресурсами. Это далеко от "бесшовной" и "нативной" интеграции, которую можно было бы ожидать, особенно для такого распространенного сценария.
Речь не идет о том, что GKE небезопасен в принципе. При наличии острой необходимости, с помощью Kubernetes Network Policies и правильной, хотя и многоуровневой, конфигурации GCP Firewall (через теги узлов) можно построить надежную защиту. Но это сложнее, менее гибко и более трудоемко, чем могло бы быть, если бы интеграция с GCP Firewall на уровне идентификации подов для доступа к внешним ресурсам VPC была действительно "родной" и прямой.
Заключение
GKE – без сомнения, зрелая и мощная Kubernetes-платформа. Однако, когда речь заходит о глубокой и интуитивной интеграции с GCP Firewall на уровне отдельных подов для контроля доступа к внешним ресурсам VPC, текущая реализация "VPC-native" не всегда соответствует ожиданиям, порожденным этим термином. Управление сетевым доступом через тегирование узлов и сегрегацию Node Pool'ов – это компромисс, который увеличивает сложность, операционные издержки и потенциальный риск ошибок конфигурации.
Эта статья – в первую очередь для нас, инженеров. Это повод задуматься о том, как на самом деле устроена сетевая безопасность в кластерах Kubernetes и других окружениях, насколько глубоко мы понимаем взаимодействие с облачной инфраструктурой, и какие неявные ограничения могут влиять на наши архитектурные решения. Если даже у вендора такого масштаба, как Google, есть подобные нюансы в реализации интеграции, это лишь подчеркивает важность нашего собственного глубокого анализа и критического подхода к любым "коробочным" решениям. Возможно, обсуждение таких моментов в сообществе также подтолкнет и Google к дальнейшему совершенствованию своих продуктов, чтобы сделать их еще более удобными и по-настоящему "нативными" в самых разных сценариях.
А как вы справляетесь с предоставлением сетевого доступа для pod'ов из ваших Kubernetes кластеров? Какие решения или практики используете? Считаете ли вы текущий подход достаточным, или тоже видите пространство для улучшений? Делитесь опытом и мыслями в комментариях!