FQDN-Controller: egress в Kubernetes по именам доменов

Определяйте динамические правила исходящего трафика на основе имён хостов непосредственно в Kubernetes — без sidecar-контейнеров, прокси, сторонних CNI или внешних инструментов.

Kubernetes Network Policies — мощный инструмент, однако управление исходящим (egress) трафиком к конкретным доменам сопряжено с серьёзными трудностями. Большинство реальных приложений не обращаются к фиксированным IP-адресам: они зависят от сторонних API, облачных сервисов и сетей доставки контента (CDN) с динамическими DNS-записями. Поддержание статических списков разрешённых адресов нестабильно, чревато ошибками и нередко вынуждает команды создавать избыточно широкие правила.

В этой статье я расскажу, что стало мотивацией для создания FQDN-Controller, как он устроен изнутри и как с его помощью описывать чистые, DNS-осведомлённые политики egress-трафика нативными средствами Kubernetes.

Логотип FQDN-Controller

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!

© 2026 meganuke