Привет! Меня зовут Антон Седов, я DevOps-инженер и работаю в команде AGIMA. Мы с ребятами решили рассказать, как настраиваем Zero Trust Network Access через опенсорс-плагин Calico. Статей об этом не очень много, а у нас накопился опыт, которым хочется поделиться. Тут найдете полезную базовую информацию — на случай, если вообще про Calico не слышали. А если слышали — узнаете, как его настроить.
Что такое DevSecOps
В 2013 году методология DevOps начала объединять два мира: разработки ПО и его эксплуатации. Появились специалисты, которые хорошо понимали и программистов, и сисадминов. Они придумывали (или заимствовали) методологии и инструменты, которые автоматизировали решение типовых проблем в жизненном цикле ПО. Это позволило программистам не отвлекаться от написания кода, а системным инженерам — тюнить окружение для этого кода. Получилось неплохо.
Практики DevOps получили широкое распространение, многие проблемы разработки и эксплуатации получили свои Best Practice. Сейчас даже в маленьких командах процесс деплоя приложения настраивает отдельный специалист.
Девопсы по сей день продолжают искать способы сделать мир ПО лучше средствами автоматизации. В последнее время появилась амбициозная мысль: к мирам программистов и админов добавить третий — безопасников. Угроза для любого проекта, даже небольшого, — хакеры. Чтобы эффективнее им противостоять, рождается DevSecOps.
В сфере сетевой безопасности уже много хороших идей и инструментов, которые можно внедрять в нашу культуру и получать большой профит. Один из них — модель безопасности Zero Trust. Ее придумал Джон Киндерваг в 2010 году. А ее суть сводится к простой мысли: «Никому не доверяй, всегда проверяй».
Это логичное продолжение предыдущей и до сих пор доминирующей парадигмы безопасного периметра: «Не доверяй незнакомцам» или «Доверяй, но проверяй». Отныне все пользователи и ресурсы несут потенциальную угрозу причинения вреда нашему продукту.
Как устроен Zero Trust
Вершиной признания модели нулевого доверия стала публикация Белым домом меморандума о переводе всей критичной инфраструктуры государства на Zero Trust (ZT). Для решения этой задачи вокруг модели ZT национальный институт стандартов и технологий США (NIST) выстраивает архитектуру (Zero Trust Architecture или ZTA). Работа добралась до стадии предварительного проекта. Уже можно ознакомиться с тем, какие информационные системы мы будем строить в будущем.
В большой и сложной модели ZT выделились компоненты:
пользователи Zero Trust;
данные Zero Trust;
сети Zero Trust;
нагрузка Zero Trust;
устройства Zero Trust.
Каждый компонент — это точка фокуса вокруг уязвимого места любой сети и набор рекомендаций, как с ней работать. В этой статье сфокусируемся только на одном компоненте — сетях с нулевым доверием (Zero Trust Network Access или ZTNA).
Принципы Zero Trust Network Access
Четко сформулированных принципов ZTNA пока нет, но большинство аналитиков сходятся примерно в таких пунктах:
постоянный мониторинг и валидация сети;
присвоение наименьших возможных привилегий в сети;
нулевой дефолтный доступ к любому ресурсу в сети;
явное описание доступных коммуникаций в сети с помощью политик безопасности;
микросегментация сети (помните, нет более безопасного периметра);
мультифакторная авторизация.
Реализация всех пунктов в рамках сети любого предприятия превращается в сложнейшую задачу, решение которой пока только формируется. Но нам, девопсам, повезло: мы в своих проектах часто работаем с малыми сетями с небольшим числом узлов, управляемых одной технологией. И чаще всего в качестве такой системообразующей технологии используем Kubernetes. Для этой технологии задача построения ZTNA решаема уже сейчас.
Подходы к настройке ZTNA
Одна из стратегий решения задачи — использование Service Mesh. Эта технология — программный слой инфраструктуры, контролирующий сетевой трафик. На высоком уровне абстракции похоже на паттерн Middleware, присутствующий в большинстве фреймворков и любимый программистами.
Перехватываем весь трафик, анализируем его, блокируем ненужное, модифицируем по необходимости и пропускаем нужное. Внутри меши реализованы с использованием паттерна Sidecar. У разных Service Mesh работа проходит между третьим (сетевым) и четвертым (транспортным) уровнем OSI и/или на седьмом (прикладном). Мешей для k8s достаточно много: Istio, Linkerd, Consul. И все хорошо справляются с построением ZTNA.
Меши как технология разрабатывалась для приложений на микросервисной архитектуре и для проектов, где число под не исчисляется сотнями, могут оказаться оверхедом. Поэтому есть еще одна стратегия — это использование CNI (Container Network Interface) плагинов.
CNI — это набор спецификаций и библиотек для организации сетей на основе Linux-контейнеров. Плагины, написанные по спецификации, могут брать на себя полностью задачу по выделению ресурсов для построения сетей при появлении контейнера и освобождения ресурсов при его удалении. CNI используют Kubernetes, OpenShift, Amazon ECS, Apache Mesos и другие.
Для Kubernetes огромную популярность получил проект Calico. Он работает на третьем уровне OSI, может целиком взять на себя задачу построения сети внутри кластера, выдает отличную производительность и может решать большее число задач, чем мог бы Service Mesh.
Как построить ZTNA на основе Calico: шаг 0
Первым делом устанавливаем сам плагин. Для этого нужен простой конфиг.
# Устанавливаем Calico на k8s-кластер
$ curl https://docs.projectcalico.org/manifests/calico-typha.yaml -o calico.yaml
$ kubectl apply -f calico.yaml
# Убеждаемся, что Calico установилось нормально
$ kubectl get pods --all-namespaces | grep calico
Чтобы взаимодействовать с Calico, разработчики предоставили нам инструмент Сalicoctl. Ставим и его. Для удобства вызова инструмента лучше сразу сделать алиас. Перезапускаем шелл и проверяем, что Calicoctl работает.
$ kubectl apply -f https://docs.projectcalico.org/manifests/calicoctl.yaml
$ echo alias calicoctl="kubectl exec -i -n kube-system calicoctl -- /calicoctl --allow-version-mismatch" >> ~/.bashrc
$ exec bash
$ calicoctl get nodes
Важным инструментом обеспечения безопасности в сети являются сетевые политики. Они позволяют гибко настраивать все аспекты обмена пакетами между хостами в вашей сети. У Kubernetes есть свой набор политик, которые можно применять к неймспейсам, управлять трафиком между подами, разрешать и блокировать трафик согласно правилам по протоколам, именованным портам или номерам портов. Calico имплементирует все политики «Кубера» и дополняет их своими.
Политики Calico имеют примерно тот же функционал, что и «куберовские». Но они немного удобнее и более гибкие. С ними появляется возможность управлять трафиком глобально с помощью GlobalNetworkPolicy — особого ресурса Calico, который создает корневую политику для всех сетевых соединений в кластере. Комбинировать оба набора политик можно как вам угодно.
Структура политик очень проста, интуитивно понятна и легко читается. Основные сущности, с которыми мы в них работаем, это неймспейсы, селекторы и правила. Все они отлично описаны в документации Calico. Это очень гибкие инструменты. Они позволяют описать любой трафик как внутри кластера, так и коммуникации с внешним опасным миром.
Как построить ZTNA на основе Calico: шаг 1
Теперь у нас есть все знания и инструменты, необходимые для наведения порядка в стиле ZTNA. Следующий шаг — воплощение Zero Trust: запрещаем весь трафик. Мы ведь никому не доверяем.
Файл default-traffic-block.yaml:
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: default-deny
spec:
selector: all()
types:
- Ingress
- Egress
$ calicoctl apply -f - < default-traffic-block.yaml
# Проверяем, что политика применилась
$ calicoctl get gnp
Как всегда, есть нюансы. Если применить этот конфиг на боевом кластере, всё сломается, так как честно блокируется весь трафик, включая DNS. Поды «потеряют» друг друга. Разработчики предлагают нам чуть другой конфиг, который открывает подам 53 порт. Похоже, это хороший компромисс, а не отступление от принципов ZTNA, где каждому ресурсу нужно прописывать вручную политики безопасности. Будем считать это следованием принципу Don’t Repeat Yourself из мира разработки.
Файл default-traffic-block.yaml:
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:
ports:
- 53
Как построить ZTNA на основе Calico: шаг 2
Этот шаг надо провести итерационно со всеми подами всех проектов в кластере. Сначала выявить назначение пода с всеми внешними и внутренними интеграциями. Затем для каждой интеграции:
выяснить протокол и порт, по которой она живет;
написать NetworkPolicy для Ingress (входящего трафика) и для Egress (исходящего трафика).
Рассмотрим этот алгоритм на примере веб-приложения. Я привык при работе везде, где это возможно, получать какую-то визуальную информацию о работоспособности системы. Даже если это информация ошибки в консоли браузера или логах сервера. Поэтому давайте начнем разрешать трафик с фронтовой поды.
Отступление! Для написания политик важно, как ваши поды организованы в неймспейсы. А еще наличие лейблов у ваших подов. Мы выбрали для себя схему организовывать поды по признаку окружения: develop, staging, production. Также к каждому поду добавляется лейбл role с описанием функции, которой он выполняет (front, back, database и т. д.). Какую схему именования неймспейсов выбрать не принципиально, но важно, что без этого массовые операции типа присваивания политик безопасности будут затруднены.
На фронте у нас SPA, который раздает nginx. Значит, логично открыть http-порты. Вот так может выглядеть примерный конфиг:
Файл open-nginx-http.yaml:
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
name: allow-nginx-http
namespace: production
spec:
selector: role == 'nginx'
types:
- Ingress
- Egress
ingress:
- action: Allow
protocol: TCP
destination:
ports:
- 80
- 443
egress:
- action: Allow
protocol: TCP
destination:
ports:
- 80
- 443
Открываем браузер, проверяем. Заработало? Вряд ли. У нас точно нет.
Загвоздка простая: перед фронтом стоит Ingress Controller, которому мы тоже весь трафик отрубили. Дублируем конфиг меняя неймспейс на namespace: ingress-nginх, а селектор — на name == ‘ingress-nginx’. Вот теперь заработало.
Дальше видим сообщения об ошибке от SPA о том, что бэкенд молчит. Открываем бэкенд — он жалуется на базу данных, Redis и так далее. Работа с этими задачами ничем не отличается от работы с фронтом, и конфиги будут очень похожими.
Конечно, приведенные примеры простые. У Calico есть еще много более продвинутых способов решения задачи построения ZTNA. Но для них придется проштудировать доки Calico и, может быть, «Кубера», если вы еще не добрались там до политик безопасности.
Приятный бонус от использования K8s и Calico — в очень хорошей переносимости между проектами. Написав один раз конфиги, мы можем переиспользовать их и не решать эту задачу снова и снова.
Вместо послесловия
Индустрии понадобилось 12 лет, чтобы согласиться на такой уровень паранойи. Слишком много громких утечек персональных данных и резонансных взломов прошло в последнее время. И при этом с распространением облаков и контейнеризации, всё меньше мы контролируем инструменты, с которыми работаем.
Вспомните, как долго сетевые инженеры сопротивлялись внедрению докера в рабочие процессы. Уже не мы конфигурируем операционную систему, не мы ставим там критически важные для работы пакеты. Со временем это успокоилось, появлялись образы, создаваемые вендорами, сканеры, проверяющие образы слой за слоем. Мы сами научились создавать свои образы.
Однако теперь образы крутятся в «кубере», который может стать вектором атаки. «Кубер» может работать в облаке, которое тоже может быть уязвимо. При этом вся ответственность за проект целиком на нас. Исходя из всего этого, мы считаем, что модель Zero Trust позволяет нам значительно снизить риск оказаться жертвой атаки для наших клиентов и внедряем ее в наши процессы девопса.
Рассказывайте в комментариях, как вы обеспечиваете безопасность проектов. Если есть вопросы — задавайте. Буду рад ответить.
Комментарии (4)
wellprog
01.12.2022 11:05Честно говоря не совсем понятно зачем именно такие жесткие требования к контейнерам. Например у нас есть контейнер в котором крутится приложение Х. Оно слушает только один единственный порт Y. В самом контейнере больше ничего живого нет, и мы еще ограничиваем этот контейнер именно этим портом, с учетом того что других нет. И как это соотносится с безопасностью?
Возможно таким образом мы отсекаем потенциальные дополнительные открытые порты разработчиком....OrionShah
02.12.2022 12:57Это нужно если злоумышленник попал в контейнер Y с другим приложением, которое обычно не отправляет запросы к приложению X. Так уже будет сильно меньше диапазона для дальнейшей атаки, а в некоторых случаях даже в интернет не попасть, чтобы что-то напрямую запросить/отправить.
И да, если сам разработчик открыл порты лишние "для себя".
WAR-S
Yaml отъехал, а так статья прикольная)
akili Автор
спасибо) ямлики привели в чувство)