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

fqdn-network-policy: ограничиваем исходящий трафик в Kubernetes по доменам

Попробуй решить на первый взгляд простую инженерную задачу: у тебя в кластере Kubernetes работает платёжный сервис checkout, которому разрешено общаться во внешнем мире ровно с двумя эндпоинтами — API Stripe и webhook-сервером GitHub. Любой другой исходящий трафик должен быть заблокирован, чтобы при взломе контейнера злоумышленник не слил данные на сторонний сервер.

Ты открываешь спецификацию ресурса Kubernetes NetworkPolicy, смотришь в секцию egress и упираешься в стену. Стандартный механизм умеет фильтровать трафик только по IP-адресам и маскам CIDR через ipBlock. Но у Stripe, GitHub или AWS S3 нет фиксированных статичных адресов: они живут за CDN с Anycast-маршрутизацией, где адреса сменяются каждые несколько минут, а TTL в DNS составляет считанные секунды.

В итоге перед инженером встаёт дилемма: либо разрешить подам доступ во весь интернет (0.0.0.0/0), либо затевать сложнейшую миграцию кластера на специализированный CNI вроде Cilium или внедрять тяжёлый Service Mesh с sidecar-прокси Envoy в каждом поде.

Открытый контроллер fqdn-network-policy от разработчика Kunal решает эту проблему без лишних костылей. Инструмент вводит ресурс FQDNNetworkPolicy, который берёт на себя мониторинг DNS-имён и автоматически генерирует под капотом стандартные правила NetworkPolicy, сохраняя совместимость с любым CNI.

В чём боль стандартного Egress: почему CIDR не дружит с современным интернетом

В материале Настройка Network Policies в Kubernetes мы детально разбирали паттерн «Default Deny» и трансляцию правил политик в таблицы iptables.

Ядро Linux и сетевые фильтры работают на уровне пакетов L3/L4, проверяя IP назначения. Когда приложение вызывает fetch('https://api.stripe.com'):

  1. Приложение запрашивает CoreDNS и получает актуальный IP (например, 3.18.12.10).
  2. Приложение открывает TCP-соединение на этот IP через порт 443.

Пакет NetworkPolicy не знает доменное имя: он видит только попытку отправить SYN-пакет на 3.18.12.10. Если зашить этот IP вручную, через полчаса CDN изменит маршрут, трафик пойдёт на 3.18.12.11, и платежи упадут по таймауту.

Архитектура MultiResolver и автоматическая защита от DNS Rebinding

Контроллер fqdn-network-policy написан на Go на базе controller-runtime и библиотеки miekg/dns, работая в качестве классического Kubernetes-оператора:

  1. Стратегия MultiResolver: Разные DNS-серверы могут возвращать разные Anycast IP для одного домена. Чтобы избежать рассинхронизации между подом и контроллером, fqdn-network-policy параллельно опрашивает четыре публичных резолвера: Cloudflare (1.1.1.1), Google (8.8.8.8), Quad9 (9.9.9.9) и OpenDNS (208.67.222.222). Результаты объединяются, перекрывая до 85% расхождений CDN.
  2. Защита от DNS Rebinding: Если внешний домен внезапно отрезолвится в адрес внутренней сети (вектор SSRF), контроллер автоматически отбросит такие ответы, фильтруя приватные подсети RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), петлевые адреса (127.0.0.0/8) и link-local (169.254.0.0/16).
Внимание к задержкам Anycast

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

Пошаговая настройка FQDNNetworkPolicy и трансляция в iptables

Контроллер устанавливается через Helm-чарт с включением Leader Election:

# Развёртывание контроллера в кластере
helm install fqdn-network-policy charts/fqdn-network-policy/ \
  --namespace fqdn-network-policy-system \
  --create-namespace \
  --set replicaCount=2 \
  --set leaderElection.enabled=true \
  --set multiResolver.enabled=true

После установки мы объявляем желаемые правила исходящего трафика на понятном языке доменов:

apiVersion: netsec.kunal.dev/v1alpha1
kind: FQDNNetworkPolicy
metadata:
  name: allow-payment-apis
  namespace: payments
spec:
  mode: Enforce
  podSelector:
    matchLabels:
      app: checkout
  egress:
    - match: api.stripe.com
      ports:
        - port: 443
          protocol: TCP
    - match: api.github.com
      ports:
        - port: 443
          protocol: TCP
  resolutionTTLOverride: 30

Контроллер подхватывает CRD, запрашивает DNS, разрешает трафик к CoreDNS (порт 53) и генерирует в том же namespace стандартный объект networking.k8s.io/v1 NetworkPolicy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: fqdnnp-allow-payment-apis
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: checkout
  policyTypes:
    - Egress
  egress:
    - ports:
        - port: 53
          protocol: UDP
        - port: 53
          protocol: TCP
    - to:
        - ipBlock:
            cidr: 3.18.12.10/32
        - ipBlock:
            cidr: 140.82.121.6/32
      ports:
        - port: 443
          protocol: TCP

Твой сетевой плагин (Calico, Flannel, AWS VPC CNI или Azure CNI) подхватывает сгенерированный манифест как обычную родную политику.

Баланс между удобством и безопасностью: где ждать подводных камней

Главное достоинство fqdn-network-policy — нулевой порог входа. Не нужно переустанавливать кластер или запускать Envoy-сайдкары, съедающие память.

Но инженер обязан трезво видеть границы применимости:

  • Это фильтрация L3/L4, а не L7: контроллер не инспектирует HTTP-заголовки или пути URI. Для точечной валидации методов потребуется API Gateway или Service Mesh.
  • Fail-Closed поведение: если контроллер потеряет связь с DNS-серверами, старые IP продолжат действовать до истечения кэша, после чего исходящий трафик будет заблокирован. Поэтому в проде запускайте контроллер минимум в двух репликах.

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