Привет! Я Максим Рязанцев, DevOps-инженер в AGIMA. Мы разрабатываем большие проекты для больших компаний — поэтому много внимания уделяем безопасности. Новости о новых утечках данных или взломах прилетают чуть ли не каждую неделю. Работать зачастую приходится с облачными технологиями. И всё это увеличивает паранойю: не хочется профакапиться. Бороться с паранойей помогает модель Zero Trust. Как мы с ней работает, подробно опишу в этой статье.

Что такое Zero Trust

В последние годы подход к безопасности изменился. Мы постепенно отказываемся от понятий «доверенный инсайдер» и «недоверенный аутсайдер». Вместо этого переходим к модели, в которой все пользователи считаются ненадежными. Ее называют по-разному, но самый распространенный термин — Zero Trust.

Сеть с нулевым доверием — это подход к сетевой безопасности, основанный на принципе, что сеть всегда считается враждебной. Это прямо противоположно подходам периметра и «сегментации», которые сосредоточены на разделении сети на доверенные и ненадежные сегменты.

Крупные нарушения обычно начинаются с незначительной компрометации всего лишь одного компонента, но затем злоумышленники используют сеть для бокового перемещения к более важным целям: данным вашей компании или клиентов. В модели зоны или периметра злоумышленники могут свободно перемещаться внутри периметра или зоны после того, как они скомпрометировали одну конечную точку.

Требования сети с нулевым доверием

Сети с нулевым доверием полагаются на средства контроля доступа к сети с особыми требованиями:

1. Все сетевые подключения подлежат принудительному контролю (а не только те, которые пересекают границы зоны).

2. Установление личности удаленной конечной точки всегда основывается на нескольких критериях, включая надежные криптографические доказательства личности. В частности, идентификаторов сетевого уровня, таких как IP-адрес и порт, недостаточно, поскольку они могут быть подделаны враждебной сетью.

3. Все ожидаемые и разрешенные сетевые потоки разрешены явно. Любое соединение, не разрешенное явно, запрещается.

4. Скомпрометированные рабочие нагрузки не должны иметь возможности обойти применение политик.

5. Опциональное полное шифрование трафика. Многие сети с нулевым доверием также полагаются на шифрование сетевого трафика, чтобы предотвратить раскрытие конфиденциальных данных враждебным организациям, отслеживающим сетевой трафик. Это не абсолютное требование, если частные данные не передаются по сети, но для соответствия критериям сети с нулевым доверием шифрование должно использоваться при каждом сетевом соединении, если оно вообще требуется. Сеть с нулевым доверием не делает различий между доверенными и ненадежными сетевыми ссылками или путями. Даже если шифрование не используется для обеспечения конфиденциальности данных, криптографические доказательства подлинности по-прежнему используются для установления личности.

Для реализации всех требований нам в большинстве случаев понадобится комплексное решение из Network Policies и сервис-меша. В K8s за реализацию ресурсов NetworkPolicy отвечают CNI-плагины, а подтверждение источника трафика и его точки назначения выполняет сервис-меш. При этом его функции на этом далеко не ограничиваются, но мы рассмотрим только то, что нас может интересовать в рамках сетевой безопасности. Сервис-меш использует mTLS для двусторонней подписи источников трафика:

Есть пример успешной интеграции CNI Calico и сервис меша Istio. Istio для авторизации трафика использует RBAC-модель K8s и в соответствии с этим NetworkPolicy Calico будет выглядеть следующим образом:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
 name: summary
spec:
 selector: app == 'summary'
 ingress:
   - action: Allow
     source:
       serviceAccounts:
         names: ['customer']
 egress:
   - action: Allow

Доступ входящего трафика в поды с лейблом Summary будет разрешён только c подов, созданных от сервисного аккаунта Customer.

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

Ситуация, когда нужно развернуть в одном кластере dev/stage, а то и dev/stage/prod среды, встречаются часто. Самой очевидной проблемой может стать возможность влияния подов из одного окружения на другое, при наличии сетевой доступности между ними. Случиться это может при банальных ошибках конфигурирования, и вот ваш Dev Pod вовсю изменяет данные стейдж-контура, на котором идет бизнес-тест. А хочется, чтобы было так:

Для небольших кластеров, в которых предполагается цикл разработки одного проекта, внедрение сервис-меша видится оверхедом. Для реализации сетевой изоляции окружений нам вполне хватит использования Network Policies. 

Давайте посмотрим на некоторые особенности CNI плагинов, способных нам реализовать сетевые политики.

Сравнение CNI

Weaveworks:

  • Weave использует VxLAN для обеспечения сетевой доступности подов на разных хостах.

  • WeaveNet — это полный меш из коробки. Каждый пир должен быть связан с каждым, что влияет на производительность при увеличении количества хостов (> 100). Для решения проблемы у Weave есть multi-hop routing, что при ограничении количества соединений у пиров оставляет сеть связной. 

  • Использует механизм Fast Datapath openVSwitch модуля ядра для увеличения скорости доставки пакетов. 

  • Weave реализует ванильные сетевые политики K8s. 

  • Возможность шифровать весь трафик через IP Encapsulating Security Payload (ESP) of IPsec.

  • Есть инструмент GUI мониторинга сети WeaveScope.

Calico:

  • Для маршрутизации пакетов между узлами Calico может использовать протокол маршрутизации BGP, что в случае нахождения всех хостов в одной подсети позволяет отказаться от туннелирования.

  • В случаях, когда избежать туннелирования невозможно, Calico предлагает использовать IP-IP или VxLAN.

  • В Calico два основных компонента: Bird и Felix. Felix запускается демоном на всех хостах и обеспечивает работу с IPtables относительно добавляемых сетевых политик. Bird — это BGP-peer. Он создает полный меш, связываясь с остальными пирами, и обеспечивают синхронизацию сетевых маршрутов между хостами.

  • Так как Bird старается быть полым мешем, на кластерах с большим количеством нод будет потеря в производительности. Для решения проблемы используют Route Reflectors.

  • Calico реализует все ванильные K8s Network Policies и добавляет к ним расширение собственными. Конфигурация собственных сетевых политик Calico осуществляется инструментом Calicoctl.

  • Calico для шифрования трафика может использовать Wireguard.

  • Инструменты визуализации есть только в Enterprise-версии.

Cilium:

  • Из коробки использует VXLAN, но его можно настроить для использования с Kube-router для маршрутизации через BGP.

  • Заменяет IPtables для фильтрации пакетов и балансировку на eBPF. eBPF позволяет запускать изолированные приложения в ядре операционной системы. Он используется для безопасного и эффективного расширения возможностей ядра без необходимости изменения его исходного кода или загрузки модулей.

  • Реализует Network Policies K8s и дополняет собственными.

  • Для шифрования трафика может использовать как IPsec так и Wireguard.

  • Cilium может реализовывать сетевые политики L7 из коробки, без сервис-меша (фильтры по DNS, HTTP, etc).

  • Есть инструмент мониторинга сети Hubble.

Из всего вышеперечисленного от Weave мы можем отказаться по причине отсутствия в нём глобальных сетевых политик, так как для организации ZTN нам нужно сначала всё запретить, а затем разрешить только нужное.

Из оставшихся Calico и Cilium, последний выглядит хорошим выбором как по бесплатному функционалу, так и по использованным технологиям под капотом. Думаю, в перспективе нескольких лет Cilium может стать широко распространенным решением в организации сетей в K8s. Некоторые облачные провайдеры уже включают его как опцию для реализации сетевых политик наряду с Calico при развертывании кластеров.

Но пока Cilium остается необходимым решением только лишь для больших инсталляций, которые порождают много правил IPtables. Во всех остальных случаях, будь то желание видеть свой трафик в красивом GUI или просто начать использовать технологии моложе 25 лет, Cilium будет факультативом.

Итак, Calico

Для наглядного примера использования политик развернем стенд со Strapi в трёх контурах: Dev, Stage и Prod. База данных для каждого контура будет своя. 

Целевая модель для политик в кластере — это запретить как входящий, так и исходящий трафик для всех Workload-подов, если явно не задано разрешающее правило.

Во многих облаках Calico либо устанавливается по умолчанию при развертывании кластера, либо есть опция с выбором устанавливаемого CNI-плагина. Но просто установленного Calico в кластер нам будет недостаточно.

У Kubernetes есть свой набор политик, которые можно применять к неймспейсам, управлять трафиком между подами, разрешать и блокировать трафик согласно правилам по протоколам, именованным портам или номерам портов. Calico имплементирует все политики «Кубера» и дополняет их своими. Чтобы взаимодействовать с политиками Calico, разработчики предоставили нам инструмент calicoctl, установим его как pod в кластере и для удобства вызова инструмента сразу сделаем алиас.

$ kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/calicoctl.yaml

$ echo alias calicoctl="kubectl exec -i -n kube-system calicoctl -- /calicoctl --allow-version-mismatch" >> ~/.bashrc

$ exec bash
$ calicoctl get nodes

Применяем сетевые политики по модели ZTN

Политики Calico делятся на 2 типа: Global и Namespaced. Первые позволяют применять правила глобально на все объекты кластера, в том числе хосты. Вторые действуют в рамках неймспейса.

Мы организуем все наши Policy в набор файлов:

  • calico-gp.yml — глобальные политики;

  • calico-np-ingress.yml — политики для ингресса;

  • calico-np-dev.yml — политики контура Dev;

  • calico-np-stage.yml — политики контура Stage;

  • calico-np-prod.yml — политики контура Prod.

Для написания политик важно, как ваши поды организованы в неймспейсы. А еще наличие лейблов у ваших подов. Мы выбрали для себя схему организации по признаку окружения: Dev, Stage, Prod, а также по функциональной роли компонента. 

Иными словами, к подам добавляем лейбл Role с описанием функции, которую он выполняет (Front, API, DB и т. д.). Какую схему именования неймспейсов выбрать, непринципиально. Важно, что это необходимо для массовых операций присваивания политик безопасности.

Глобальные политики

Начнём применять политики с запрещения любого трафика во всех неймспейсах, кроме системных:

calico-gp.yml:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
 name: default-deny
spec:
 namespaceSelector: has(projectcalico.org/name) && projectcalico.org/name not in {"kube-system", "calico-system", "calico-apiserver"}
 types:
   - Ingress
   - Egress
 egress:
   - action: Allow
     protocol: UDP
     destination:
       selector: k8s-app == "kube-dns"
       ports:
       - 53


Тут мы исключаем системные неймспейсы от применения запрещающего правила. Там, где запрещаем, оставляем «форточку» для исходящего трафика на поды с лейблом k8s-app=kube-dns на 53-й порт, чтобы поды друг друга видели по имени. Сам Coredns находится в неймспейсе Kube-system, так что подключения к нему не фильтруются:

Использование Projectcalico.org/name позволяет работать с именани неймспейсов напрямую, без использования дополнительных меток на последних.

Применяем правило:

calicoctl apply -f - < calico-gp.yml

Трафик должен полностью блокироваться, и попытка открыть любой из контуров сайта в браузере приведёт к таймауту соединения, Nginx Ingress не ответит.

Политики ингресса

Далее пишем правила для ингресс-контроллера:
calico-np-ingress.yml:

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
 name: allow-nginx-ingress
 namespace: ingress-nginx
spec:
 selector: app.kubernetes.io/name == "ingress-nginx"
 types:
   - Ingress
   - Egress
 ingress:
   - action: Allow
     protocol: TCP
     destination:
       ports:
         - 80
         - 443
   - action: Allow
     protocol: TCP
     source:
       nets:
         - 10.129.0.10/32 # master ip 
     destination:
       ports:
         - 8443
 egress:
   - action: Allow
     protocol: TCP
     destination:
       selector: role == "api"
       namespaceSelector: has(projectcalico.org/name) && projectcalico.org/name in {"prod", "dev", "stage"}
       ports:
         - 1337

В отличие от предыдущего правила (kind: GlobalNetworkPolicy), это Namespaced (kind: NetworkPolicy). Оно применяется ко всем удовлетворяющим Secetor-объектам неймспейса, в которое деплоится. В нашем случае selector: app.kubernetes.io/name == "ingress-nginx" — это метка ингресс-контроллера. Мы разрешаем на него любой входящий трафик на порты 80 и 443, а также разрешаем подключение с IP  10.129.0.10 на порт 8443. Это необходимо, чтобы мог отработать Admission Webhook у ингресс-контроллера. 10.129.0.10 — это IP мастер-узла. В облачных K8s вы, как правило, не увидите своих мастер-нод и не сможете посмотреть метки на них. Вероятнее всего, селектор вида has(node-role.kubernetes.io/master) будет работать корректно, но я предпочёл на опираться на информацию, которую вижу явно — IP мастера показан в дашборде облака. 

Что касается исходящего трафика, тут нам нужно преодолеть барьер между неймспейсами. Поэтому указывает селектор не только пода, но и неймспейса в сторону которых разрешаем отправить трафик:

  destination:
       selector: role == "api"
       namespaceSelector: has(projectcalico.org/name) && projectcalico.org/name in {"prod", "dev", "stage"}
       ports:
         - 1337

calicoctl apply -f - < calico-np-ingress.yml

После применения правила мы можем увидеть в браузере:

Ингресс-контроллер ответил, двигаемся дальше.

Политики деплоймента

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

calico-np-dev.yml

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
 name: allow-api
 namespace: dev
spec:
 selector: role == "api"
 types:
   - Ingress
   - Egress
 ingress:
   - action: Allow
     protocol: TCP
     source:
       selector: app.kubernetes.io/name == "ingress-nginx"
       namespaceSelector: projectcalico.org/name == "ingress-nginx"
     destination:
       ports:
         - 1337
 egress:
   - action: Allow
     protocol: TCP
     destination:
       selector: role == "db"
       ports:
         - 3306
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
 name: allow-db
 namespace: dev
spec:
 selector: role == "db"
 types:
   - Ingress
 ingress:
   - action: Allow
     protocol: TCP
     source:
       selector: role == "api"
     destination:  
    ports:
         - 3306

Для пода API мы разрешаем принимать подключения на 1337 с пода с меткой

app.kubernetes.io/name == "ingress-nginx" из неймспеса Ingress-nginx.

 source:
       selector: app.kubernetes.io/name == "ingress-nginx"
       namespaceSelector: projectcalico.org/name == "ingress-nginx"

Исходящий трафик из API разрешаем до пода DB в том же неймспейсе, поэтому указываем только Selector.

   destination:
       selector: role == "db"
       ports:
         - 3306

Входящий трафик в под DB разрешаем из пода API.

  source:
       selector: role == "api"
     destination:     
       ports:
         - 3306

Это всё, приложение в Dev-контуре работает. Копируем правила из calico-np-dev.yml в calico-np-stage.yml и calico-np-prod.yml, изменяем неймспейсы и применяем.

calicoctl apply -f - < calico-np-dev.yml

calicoctl apply -f - < calico-np-stage.yml

calicoctl apply -f - < calico-np-prod.yml

Тестирование изоляции

Главная ценность ZTN-подхода — любая новая рабочая нагрузка, запущенная в кластере, будет изолирована на L3-L4-уровне как от всего кластера, так и от внешней сети. 

Попробуем запустить в кластере Pod с сетевыми утилитами и посмотрим, какие изменения в правила нам нужно внести для обеспечения его сетевой доступности с остальными подами внутри и за пределами неймспейса.

netshot.yml

apiVersion: v1
kind: Pod
metadata:
 name: netshot
 labels:
   role: debug
spec:
 containers:
 - name: netshot
   image: nicolaka/netshoot
   command:
     - sleep
     - "10000"

Заранее дали поду лейбл role=debug, он нам пригодится в дальнейшем. Заходим в него и осматриваемся:

Имена подов резолвятся внутри и за пределами неймспейса. Но порты фильтруются:

Для открытия доступа к поду DB нам нужно модифицировать правила Ingress для DB:

Было:

    source:
       selector: role == "api"

Стало:

source:
       selector: role in {"api", "debug"}

Ещё нужно добавить правило в calico-np-dev.yml для debug, разрешающее исходящий трафик:

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
 name: allow-debug
 namespace: dev
spec:
 selector: role == "debug"
 types:
   - Egress
 egress:
   - action: Allow
     protocol: TCP
     destination:
 namespaceSelector: projectcalico.org/name in {"stage", "dev"}
       selector: role in {"db"}
       ports:
         - 3306

calicoctl apply -f - < calico-np-dev.yml

Тестируем:

Для доступа к DB в другом неймспейсе нужно добавить изменения в Ingress calico-np-stage.yml не только в selector, но и добавить сам namespaceSelector.

calico-np-stage.yml

  source:
       namespaceSelector: projectcalico.org/name in {"stage", "dev"}
       selector: role in {"api", "debug"}

calicoctl apply -f - < calico-np-stage.yml

Важно помнить, что нужно обязательно добавлять namespaceSelector в Namespaced правила в случае их пересечения границ неймспейса размещения.

Фильтрация внешнего трафика

Когда нужно ограничить доступ к Stage- и Dev-площадкам, обычно достаточно применить фильтрацию на L7-уровне через аннотации ингресса:

dev

ingress:
 enabled: true
 className: "nginx"
 annotations:
   nginx.ingress.kubernetes.io/whitelist-source-range: "82.138.46.14"
 hosts:
   - host: strapi-dev.ryazantseff.com
     paths:
       - path: /
         pathType: Prefix

Тут мы ограничиваем доступ к приложению до одного IP (можно несколько через запятую, можно CIDR вместо IP) — 82.138.46.14. Например, это офис, где работают разработчики или внешний адрес VPN. Нужно только убедиться, что облачный балансировщик отправляет трафик сразу на нужные ноды кластера и что трафик внутри не перенаправляется через Kube-proxy. Обычно для этого достаточно изменить конфигурацию LoadBalancer сервиса:

kubectl patch svc nginx-ingress-controller -p

'{"spec":{"externalTrafficPolicy":"Local"}}'

Могут понадобиться дополнительные действия, обратитесь к документации своего облачного провайдера.

Но бывает, что нужно заблокировать доступ к кластеру целиком для диапазона IP. Для этого дополним calico-gp.yml:

---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkSet
metadata:
 name: deny-cidrs
 labels:
   ip-deny-list: 'true'
spec:
 nets:
   - 82.138.46.14/32
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
 name: deny-cidrs
spec:
 namespaceSelector: has(projectcalico.org/name) && projectcalico.org/name not in {"kube-system", "calico-system", "calico-apiserver"}
 types:
   - Ingress
 ingress:
   - action: Deny
     source:
       selector: ip-deny-list == 'true' && !has(projectcalico.org/name)

kind: GlobalNetworkSet — удобная сущность, позволяет определить список CIRD-ов и использовать его в селекторах правил.

source:
       selector: ip-deny-list == 'true' && !has(projectcalico.org/name)

Вышеуказанная конструкция означает, что источник трафика не должен иметь неймспейса. Иными словами, быть внешним (!has(projectcalico.org/name)) и попадать в диапазон GlobalNetworkSet с лейблом ip-deny-list == 'true'.

calicoctl apply -f - < calico-gp.yml

Итог

Приятный бонус от использования K8s и Calico в очень хорошей переносимости между проектами. Написав один раз конфиги, мы можем переиспользовать их и не решать эту задачу снова и снова.

Для типового проекта размещаемого в K8s нам необходимо будет написать в среднем 4–6 правил для одного окружения и скопировать их на оставшиеся (Stage и Prod) с небольшими изменениями. Описанный выше подход при небольших временных затратах приносит большую ценность. Если вы еще не начали использовать сетевые политики в каждом своем проекте, то, кажется, сейчас самое время. 

Индустрии понадобилось 12 лет, чтобы согласиться на такой уровень паранойи. Слишком много громких утечек персональных данных и резонансных взломов прошло в последнее время. И при этом с распространением облаков и контейнеризации, всё меньше мы контролируем инструменты, с которыми работаем. При этом вся ответственность за проект целиком на нас. И модель Zero Trust позволяет нам значительно снизить риск оказаться жертвой атаки для наших клиентов. Поэтому мы внедряем ее в наши процессы DevOps.


Если хотите обсудить статью, узнать больше о разработке и DevOps, подписывайтесь на наш телеграм-канал AGIMA Dev.

Комментарии (1)


  1. olku
    05.07.2023 19:45

    Istio Ambient mesh будет ещё одной альтернативой бессайдкаровым CNI, когда из альфы выйдет.