Привет, Хабр! Меня зовут Никита Бахилин, я студент DevOps-курса YADRO. Во время обучения мы с сокурсником Даниилом Уткиным столкнулись с неочевидной проблемой при развертывании кластера Kubernetes. Мы не могли сделать пинг внутри пода K8s. Материалов, которые полноценно описывали бы проблему, я не нашел, поэтому мы написали эту статью. Надеемся, она поможет тем, кто только начинает работать с известным оркестратором.

Предыстория

Настал момент знакомства с Kuber****s, или K8s, в рамках DevOps-курса. Задача стояла сложная для опытного инженера, но трудная и непонятная для человека, который никогда не сталкивался с K8s.

Задача: Организовать k8s-кластеры в ручном режиме с помощью kubeadm и kubectl на базе cri-o (1.28+) и использовать Calico как CNI-плагин.

Кластер доступен для взаимодействия через kubectl, команда возвращает корректную информацию о кластере. Есть возможность сделать ping 8.8.8.8 с образом busybox.

Поясним некоторые термины для новичков: 

  • kubectl — клиент, консольная утилита для взаимодействия с API кластеров Kubernetes. 

  • kubeadm — утилита, упрощающая настройку кластера и компонентов Kubernetes.

  • cri-o — высокоуровневый runtime для создания контейнеров, аналог containerd (Docker машет ручкой).

Обратите внимание на последний пункт задания. В нем будет основная загвоздка. 

Если для вас это плевая задачка и вам интересна работа в YADRO, участвуйте в спринт-оффере для DevOps-специалистов. Здесь вы cможете получить работу в дивизионе Телеком всего за три дня.

Начинаем решать задачу

Сразу уточним, что в этом тексте мы опустим процессы установки kubectl, kubelet, kubeadm, cri-o на ваших узлах. Мы подразумеваем, что они уже у вас установлены. 

На этом этапе все worker-ноды должны быть добавлены в наш кластер. Чтобы это проверить, можете воспользоваться следующей командой на master-узле:

$ kubectl get nodes
on control-plane node
on control-plane node

Результат команды показывает, что у нас есть два worker-узла и один управляющий. Если у вас примерно такой же stdout и STATUS: Ready всех доступных в кластере узлов, то можно идти дальше. 

Выполняем команду, которую требует задание:

$ kubectl run -it --tty test --image=busybox -- sh
run command
run command

Распарсим команду, которую только что отправили безжалостному kube-api-server:

  • -it — интерактивный режим. 

  • - - tty — явно указываем, что нужен псевдотерминал (-t уже есть, можно не указывать).

  • test — название будущего пода.

  • -- image=busybox — образ busybox (базовые Unix-утилиты).

  • - - sh — запуск оболочки Shell внутри контейнера.

Мы запускаем под (pod) — базовую единицу управления в кластере, который представляет собой группу одного или нескольких контейнеров, разделяющих между собой сетевые ресурсы и хранилище.

После запуска пода нас перебрасывает внутрь контейнера test. Внутри — стандартный Shell.

Теперь проверяем возможность выполнить команду ping 8.8.8.8:

/ # ping 8.8.8.8
test (busybox)
test (busybox)

И тут мы ловим запрет — permission denied— на отправку ICMP-пакетов внутри контейнера. Главное — не паниковать.

Напомню, что команда ping — это базовая утилита для проверки доступности узла в сети с помощью ICMP-протокола. Последний работает на сетевом уровне вместе с IP, но все равно инкапсулируется в IP-пакет с добавлением соответствующих заголовков. Можно убедиться в этом, используя утилиту для анализа сетевого трафика — wireshark.

8.8.8.8 — публичный DNS-сервер Google. 
8.8.8.8 — публичный DNS-сервер Google. 

Решение проблемы

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

Мы перепробовали множество способов решения проблемы:

  • Писали манифесты.

  • Добавляли securityContext в поды.

  • Пытались настраивать параметры безопасности кластера.

Когда ни один из вариантов не сработал, обратились к куратору DevOps-курса Артуру Франку, старшему инженеру по разработке ПО в YADRO. Он ответил, что образ должен запускаться напрямую из консоли без дополнительных манифестов и настроек.

Это заставило задуматься. Значит, проблема не в Kubernetes, а на более низком уровне.

Начали анализировать систему, переключив внимание с уровня контейнеров на уровень хостовой ОС (в нашем случае — Ubuntu).

И тут мы здороваемся с мистером cri-o.

Cri-o — это реализация Kubernetes CRI (Container Runtime Interface), позволяющая использовать среды выполнения, совместимые с OCI (Open Container Initiative).

Тут мы просто обязаны познакомить вас с героями, которых в Linux/Gnu кличут как capabilities.

Сapabilities — это механизм в Linux, который позволяет детально управлять правами процессов без необходимости запуска от root. Они разделяют привилегии суперпользователя на отдельные «способности».

Давайте проверим capabilities нашего cri-o. Для начала проверим текущую конфигурацию cri-o, выполнив команду:

$ crio config

Как же нам понять какая именно суперспособность нужна нашему контейнеру?

Открываем в терминале документацию man по ping и ищем раздел security:

$ man ping 

Видим, что нам нужен именно NET_RAW (CAP_NET_RAW). Проверим наличие этой capability у нашего cri-o:

$ crio config | grep NET_RAW
CAP_NET_RAW
CAP_NET_RAW

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

Добавим эту способность в наше cri-o runtime:

$ vim  /etc/crio/crio.conf.d/20-default-caps.conf

Теперь список default_capabilities выглядит так:

[crio.runtime]
default_capabilities = [
   "CHOWN",
   "DAC_OVERRIDE",
   "FOWNER",
   "FSETID",
   "KILL",
   "SETGID",
   "SETUID",
   "SETPCAP",
   "NET_BIND_SERVICE",
   "NET_RAW"
]

Про каждую из этих capabilities можно почитать отдельно.

Теперь перезапустим cri-o-службу, чтобы изменения вошли в силу. Воспользуемся командной управления демонами Linux — systemctl:

$ systemctl restart crio.service

Проделываем эту махинацию на всех узлах, на которых собираемся запускать поды и где нужна поддержка ICMP-протокола.

Результат:

В итоге мы отправили 10 пакетов с эхо-запросами на DNS Google — получили 10 эхо-ответов. Проблема решена.

Что можно сделать иначе (комментарий Даниила):

«Единственное, что я сделал по-другому, — разместил default_capabilities не в файле /etc/crio/crio.conf, а в файле /etc/crio/crio.conf.d/20-default-caps.conf. Это связано с множеством удобств работы с *.d-конфигурациями (но это мое личное предпочтение).

Анализ проблемы

В чем же причина проблемы ping: permission denied (are you root?)? Она возникает из-за того, что для работы по протоколу ICMP (ping) требуется наличие capability CAP_NET_RAW, который разрешает создавать RAW- и PACKET-сокеты, работающие на сетевом и канальном уровнях соответственно.

Протокол ICMP как раз должен упаковываться сразу в IP-пакет без использования TCP и UDP, для чего ему и требуется создание RAW-сокета. Следовательно, нам нужно каким-то образом разрешить его использование. Что интересно, Wget, cURL и прочие функции будут работать без ошибок, потому что они используют стандартные сокеты.

Причины возникновения

Исходя из ченджлога cri-o, NET_RAW и SYS_CHROOT были удалены из default_capabilities в версии v1.18.0 из-за уязвимостей, связанных с их использованием. Мы посмотрели стандартные capabilities в moby/docker, и CAP_NET_RAW ими предоставляется. Хотя Docker не является эталоном безопасности, но, по всей видимости, его разработчики не видят проблемы в этом разрешении.

В дистрибутивах с поддержкой cri-o — Minikube и K3s — также нет патчей для решения проблемы. Да и в issue-трекерах cri-o, Minikube, K3s и прочих не так много упоминаний об этом. Делаем вывод: большинству поддержка этой функциональности не очень-то и требуется.

А является ли это вообще проблемой?

Ответ скорее отрицательный. В большинстве случаев во время работы программы пинги на уровне ICMP используют редко — чаще это пинги с использованием «вышележащих» протоколов.

Как правило, разработчикам используют ping внутри дебаг-контейнера или при поиске причин неполадок. Поэтому возникает вопрос: насколько обоснованно включать NET_RAW на уровне всего container runtime? Кажется, не очень. Да, это решает проблему, но создает дыру, которую умные разработчики зачем-то все-таки закрыли.

Альтернативное решение проблемы

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

Изучая Minikube и K3s, обратили внимание, что поды, которым нужна была эта функциональность, просто добавляли ее в securityContext.capabilities.add. И это, пожалуй, самое лучшее решение в данном случае, исходя из принципа минимизации привилегий.

А если все-таки хочется добиться добавления capability — например, на уровне определенных namespace или label? Тут на помощь придут способы динамического управления профилями безопасности: Kubernetes Security Profiles Operator или Dynamic Admission Control. Выбор подходящего решения — за вами!

Сталкивались ли вы с подобной проблемой, работай с ping в Kubernetes? И если да, то как ее решали? Ждем ваших историй в комментариях!

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


  1. jurikolo
    30.05.2025 14:35

    Я что-то упустил в статье или вот этот пункт в середине текста вы пробовали плохо:

    Добавляли securityContext в поды.

    ? Ведь именно это и предлагается в конце статьи как более ювелюрное решение.


    1. mard_en
      30.05.2025 14:35

      Видимо пытались пофиксить правами


    1. Bakhilin Автор
      30.05.2025 14:35

      Спасибо за обратную связь!
      Статья преследует следующие цели:

      1. Разобраться почему именно мы не можем отправлять пакеты 'наружу'. (Собственно и название статьи говорит само за себя)

      2. Предложить решение, соответствующее условиям задания в лоб.

      3. Показать как нужно делать.


  1. rendov
    30.05.2025 14:35

    Материалов, которые полноценно описывали бы проблему, я не нашел, поэтому мы написали эту статью

    А разве детская проблема, об которую спотыкались все, кто мало мальски пробовал использовать rootless в podman/docker нуждается в статье? Тем более, которую можно ужать до "Впишите в конфигNET_RAWно секьюрность всех запускаемых контейнеров рухнет"? Вот чел за 4 минуты уложился аж 4 года назад. А вот самое важное, про включение этого разрешения на конкретные контейнеры, через securityContextу вас буквально просто один абзац, а не основное тело статьи. В общем такое.


    1. Bakhilin Автор
      30.05.2025 14:35

      Спасибо за обратную связь!
      Статья преследует следующие цели:

      1. Разобраться почему именно мы не можем отправлять пакеты 'наружу'. (Собственно и название статьи говорит само за себя)

      2. Предложить решение, соответствующее условиям задания в лоб.

      3. Показать как нужно делать.


  1. nikweter
    30.05.2025 14:35

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

    Капать и копать - разные слова. Вы использовали первое в значении второго. Прошу прощения, если нужно было поправить как-либо иначе.


    1. Bakhilin Автор
      30.05.2025 14:35

      Благодарю за внимательность :)


  1. cdn_crz
    30.05.2025 14:35

    Ты ему слово, он тебе "Спасибо за обратную связь! Статья преследует следующие цели"


  1. trabl
    30.05.2025 14:35

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


    1. Bakhilin Автор
      30.05.2025 14:35

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