Определяйте динамические правила исходящего трафика на основе имён хостов непосредственно в Kubernetes — без sidecar-контейнеров, прокси, сторонних CNI или внешних инструментов.
Kubernetes Network Policies — мощный инструмент, однако управление исходящим (egress) трафиком к конкретным доменам сопряжено с серьёзными трудностями. Большинство реальных приложений не обращаются к фиксированным IP-адресам: они зависят от сторонних API, облачных сервисов и сетей доставки контента (CDN) с динамическими DNS-записями. Поддержание статических списков разрешённых адресов нестабильно, чревато ошибками и нередко вынуждает команды создавать избыточно широкие правила.
В этой статье я расскажу, что стало мотивацией для создания FQDN-Controller, как он устроен изнутри и как с его помощью описывать чистые, DNS-осведомлённые политики egress-трафика нативными средствами Kubernetes.
FQDN-Controller Logo (Да, логотип сгенерирован ChatGPT — не судите строго 😆)
Требование: строгая сетевая безопасность на уровне Kubernetes
Работая над побочным проектом на базе Kubernetes — SaaS-платформой, построенной вокруг операторов, — я изолировал инфраструктуру каждого клиента в отдельном пространстве имён (namespace). Одним из ключевых требований была жёсткая сетевая изоляция: ни один клиентский namespace не должен был общаться с другим.
Для этого я применил политики «запретить всё» по умолчанию для входящего и исходящего трафика к каждому клиентскому namespace. Это заблокировало любой несанкционированный внутренний трафик — именно то, что требовалось. Однако инфраструктура каждого клиента по-прежнему нуждалась в доступе к внешним облачным сервисам. В моём случае это был AWS S3, доступный через домены вида s3.eu-west-1.amazonaws.com.
Я также хотел предоставить клиентам возможность самостоятельно управлять правилами исходящего трафика: определять белые списки по именам хостов, IP-адресам или CIDR-блокам.
После того как исходящий трафик оказался закрыт по умолчанию, встала следующая задача: найти безопасный и гибкий способ разрешить доступ к внешним доменам, необходимым как платформе, так и её пользователям.
Дилемма: запрет egress и доступ к внешним сервисам
К сожалению, стандартные Kubernetes NetworkPolicies не поддерживают полностью определённые имена доменов (FQDN, Fully Qualified Domain Names) в правилах egress — только IP-адреса или CIDR-диапазоны. Это делало их крайне неудобными: пришлось бы кропотливо выяснять все IP-адреса, которые использует S3, и вручную добавлять каждый из них в список разрешений. Хуже всего то, что эти адреса могут измениться в любой момент, делая политику неполной или вовсе бесполезной.
Это ограничение подтолкнуло меня к достаточно сложному обходному решению — установке Cilium в кластеры. Cilium требует установки дополнительного CNI или замены стандартного VPC CNI облачного провайдера. Несмотря на то что Cilium — отличное облачно-нативное решение, использовать его исключительно ради преобразования доменных имён в IP-адреса казалось явно избыточным: он добавлял ещё один уровень сложности и накладные расходы на сопровождение.
Какие же ещё варианты существуют?
Доступные варианты: всё ещё недостаточно хороши
Как уже упоминалось, Cilium — один из возможных путей, но мне хотелось что-то более лёгкое и менее инвазивное. Некоторые облачные провайдеры начали решать эту проблему: например, GKE Enterprise ввёл ресурс FQDN Network Policy, однако только в рамках корпоративного тарифа. У AWS ситуация ещё менее привлекательна: задача значится в дорожной карте уже довольно долго, но так и не реализована.
Пока что большинству пользователей Kubernetes приходится прибегать к одному из следующих вариантов:
-
Замена CNI или использование плагинов на базе eBPF: как уже говорилось, Cilium способен применять FQDN-политики egress, перехватывая DNS-запросы и управляя списками разрешённых IP с помощью CiliumNetworkPolicies. Однако это означает развёртывание нового сетевого уровня и дополнительную сложность в кластере.
-
Service Mesh или прокси: service mesh (например, Istio) или внешний прокси (например, Squid) позволяют фильтровать исходящий трафик по именам хостов на уровне Layer 7. Эти решения работают, но инвазивны: они требуют sidecar-прокси или внешней прокси-инфраструктуры и создают значительные операционные накладные расходы.
-
Фаерволы и DNS-фильтрация на стороне облачного провайдера: облачные сетевые сервисы (AWS Network Firewall, Azure Firewall и т. п.) умеют применять egress-правила на основе доменов вне кластера. Такой подход эффективен, но существует за пределами Kubernetes — управлять правилами на уровне отдельных namespace или приложений неудобно, а сами сервисы могут быть дорогостоящими и негибкими.
-
Самописные скрипты или контроллеры: некоторые реализуют скрипты, которые разрешают доменные имена в IP и динамически обновляют NetworkPolicies или облачные фаерволы. Такой DIY-подход чреват ошибками и требует повторной реализации логики, которую в идеале должен выполнять специализированный инструмент.
Решение: ладно, сделаю сам!
Взвесив все варианты и убедившись, что ни одно из существующих решений не подходит, я решил закрыть потребность собственными руками и запустил open source проект — FQDN-Controller.
Это лёгкий нативный оператор Kubernetes, устанавливаемый через Helm или kubectl. Он не требует никаких дополнительных настроек и инвазивных интеграций и позволяет описывать egress-политики с использованием полностью определённых доменных имён.
Проще говоря, оператор позволяет определять пользовательские сетевые политики в привычном формате (схожем со стандартным ресурсом NetworkPolicy) — но с FQDN вместо CIDR-блоков. Он периодически разрешает эти FQDN в IP-адреса, поддерживает их актуальность и применяет результат к стандартной NetworkPolicy.
Если вам нужно ограничить исходящий трафик по доменным именам, не переворачивая при этом всю сетевую инфраструктуру с ног на голову, — FQDN-Controller стоит попробовать!
FQDN-Controller: установка
Развернуть FQDN-Controller несложно. Проект предоставляет Helm-чарт — это самый удобный способ установить контроллер вместе с его CRD. Чарт опубликован на Artifact Hub.
Установить его можно следующим образом:
helm repo add fqdn-controller https://konsole-is.github.io/fqdn-controller/charts
helm install fqdn-controller fqdn-controller/fqdn-controller --version 0.1.0
Убедитесь, что устанавливаете контроллер в нужный namespace, например fqdn-operator.
FQDN-Controller: создание сетевой политики
Разберём простой пример использования FQDN NetworkPolicy.
apiVersion: fqdn.konsole.is/v1alpha1
kind: NetworkPolicy
metadata:
name: allow-selected-egress
spec:
podSelector:
matchLabels:
app: backend
egress:
- toFQDNS:
- github.com
ports:
- protocol: TCP
port: 443
- toFQDNS:
- example.com
ports:
- protocol: TCP
port: 80
blockPrivateIPs: true
Эта политика:
-
Распространяется на все поды с меткой
app=backendв пространстве имён default. -
Задаёт два правила egress. Первое разрешает указанным подам обращаться к
github.comпо TCP-порту 443 (HTTPS). Второе разрешает доступ кexample.comпо порту 80. -
Второе правило устанавливает
blockPrivateIPs: true— это означает, что еслиexample.comвдруг разрешится во внутренний IP (например, если этот DNS-адрес иногда указывает на внутренний адрес), такой IP будет исключён. Для этого домена допускаются только публичные IP-адреса. -
Никакой другой исходящий трафик эта политика не разрешает. Помните: после применения политики весь прочий трафик соответствующего типа блокируется по умолчанию.
После применения FQDN-Controller разрешит IP-адреса для github.com и example.com, а затем создаст стандартную NetworkPolicy с тем же именем, разрешающую egress к этим IP для выбранных подов. Если затем вывести список стандартных NetworkPolicies в namespace, вы увидите политику с именем allow-selected-egress, содержащую IP-блоки, соответствующие указанным доменам.
Проверка ресурса:
$ kubectl get fqdn
NAME READY RESOLVED RESOLVED IPS APPLIED IPS LAST LOOKUP AGE
allow-selected-egress True True 7 7 10s 10s
Полное описание ресурса:
$ kubectl get fqdn allow-selected-egress -o yaml
apiVersion: fqdn.konsole.is/v1alpha1
kind: NetworkPolicy
metadata:
creationTimestamp: "2025-07-20T19:51:18Z"
generation: 1
name: allow-selected-egress
namespace: default
resourceVersion: "32740"
uid: 533b0823-f34f-484d-b59a-7a8cc6e5cff4
spec:
egress:
- ports:
- port: 443
protocol: TCP
toFQDNS:
- github.com
- blockPrivateIPs: true
ports:
- port: 80
protocol: TCP
toFQDNS:
- example.com
enabledNetworkType: ipv4
podSelector:
matchLabels:
app: backend
resolveTimeoutSeconds: 3
retryTimeoutSeconds: 3600
ttlSeconds: 60
status:
appliedAddressCount: 7
conditions:
- lastTransitionTime: "2025-07-20T19:51:18Z"
message: The network policy resolved successfully.
observedGeneration: 1
reason: SUCCESS
status: "True"
type: Resolve
- lastTransitionTime: "2025-07-20T19:51:18Z"
message: The network policy is ready.
observedGeneration: 1
reason: Ready
status: "True"
type: Ready
fqdns:
- LastSuccessfulTime: "2025-07-20T19:52:18Z"
addresses:
- 140.82.121.3/32
fqdn: github.com
lastTransitionTime: "2025-07-20T19:51:18Z"
resolveMessage: Resolve succeeded
resolveReason: SUCCESS
- LastSuccessfulTime: "2025-07-20T19:52:18Z"
addresses:
- 23.192.228.80/32
- 23.192.228.84/32
- 23.215.0.136/32
- 23.215.0.138/32
- 96.7.128.175/32
- 96.7.128.198/32
fqdn: example.com
lastTransitionTime: "2025-07-20T19:51:18Z"
resolveMessage: Resolve succeeded
resolveReason: SUCCESS
latestLookupTime: "2025-07-20T19:52:18Z"
observedGeneration: 1
totalAddressesCount: 7
Важное замечание: поскольку FQDN-Controller опирается на DNS, убедитесь, что вашим подам разрешено обращаться к DNS-сервису кластера (например, CoreDNS). На практике, если вы используете сетевые политики для ограничения egress, потребуется правило, разрешающее DNS-запросы к сервису kube-dns — иначе ни один FQDN не сможет разрешиться и весь egress будет заблокирован (это ожидаемое поведение). Аналогичная настройка нужна в любой egress-политике, зависящей от DNS.
Подробнее о параметрах конфигурации — в репозитории на GitHub!