Как привязать контейнер без пользователя root к привилегированному порту на Linux
Одним из основных нововведений, появившихся в Podman, стала возможность запуска контейнеров без прав root.
С точки зрения безопасности это было существенным улучшением, поскольку потенциально скомпрометированный контейнер, запущенный от имени root, представляет собой угрозу безопасности для хост-системы.
Чтобы добиться аналогичного поведения, последние версии Docker поддерживают запуск демона docker в пользовательском контексте.
Запуск непривилегированных контейнеров, хотя и является более безопасным, имеет и свои недостатки, например, невозможность привязки к привилегированным портам хоста.
В этом руководстве мы узнаем, как разрешить контейнеру Docker/Podman без рута привязываться к привилегированному порту хоста на Linux.
Введение
Запуск контейнеров без рута хорош с точки зрения безопасности, но накладывает свои ограничения.
Предположим, мы используем Docker (или Podman) в режиме rootless и хотим запустить контейнер, содержащий веб-сервер Apache.
Для того чтобы сделать сервис “прозрачным” для пользователя, мы хотим прописать порт 80 на хосте на тот же порт, открываемый контейнером, и запустить контейнер таким образом:
docker run -p 80:80 http
Поскольку мы запустили команду от имени непривилегированного пользователя, то получили следующую ошибку (для удобства вывод урезан):
error while calling PortManager.AddPort(): cannot expose privileged port 80
Команда не выполнилась, так как не удалось привязаться к порту 80 на хосте, поскольку он является привилегированным.
Как решить эту проблему?
Существует множество подходов, рассмотрим некоторые из них.
Перенаправление привилегированного порта на непривилегированный с помощью правила брандмауэра
Первое возможное решение заключается в перенаправлении трафика с привилегированного порта, который мы хотим использовать, на непривилегированный порт по нашему выбору, который мы затем сопоставим с портом, открываемым контейнером.
Чтобы перенаправить один порт на другой, можно создать правило брандмауэра.
Точная команда, которую необходимо выполнить, зависит от используемого менеджера брандмауэра.
Компонент ядра Linux, обеспечивающий функциональность работы с пакетами, – это “netfilter“.
В современных дистрибутивах Linux для “общения” с этим компонентом из пользовательского пространства используется фреймворк “nftables“.
Однако поскольку непосредственное создание правил nftables может оказаться утомительным занятием, чаще всего мы обращаемся к менеджерам конфигурации межсетевых экранов более высокого уровня.
Создание правила с помощью firewalld
Если вы читали нашу вводную статью о firewalld, то уже знаете, что управление им осуществляется с помощью утилиты firewall-cmd, которая не имеет собственного способа задать правило перенаправления портов.
Для этого нам приходится “возвращаться” к “прямому” правилу, которое позволяет взаимодействовать с брандмауэром на более низком уровне.
Чтобы задать “прямое” правило, мы запускаем утилиту с опцией –direct.
Предположим, что мы хотим перенаправить трафик с порта 80 на порт 8080 на localhost, для этого нужно выполнить команду:
sudo firewall-cmd --direct --add-rule ipv4 nat OUTPUT 0 -p tcp --dport=80 -o lo -j REDIRECT --to-port=8080
Обратите внимание на два момента: во-первых, приведенная выше команда добавляет правило в зону firewalld “default”, поскольку мы не указали ее явно (зоны – это абстракции, связанные с firewalld.
Во-вторых, правило не является постоянным и не переживет перезагрузки или перезапуска службы firewalld.
Для того чтобы сделать его постоянным, необходимо использовать опцию –permanent и перезапустить брандмауэр, выполнив команду:
sudo firewall-cmd --reload
Создание правила с помощью утилит iptables или nft
Синтаксис, используемый для задания прямых правил firewalld, как вы видите выше, похож на синтаксис, используемый в iptables.
Если вам (как и мне) это больше нравится, то можно задать правило, используя непосредственно утилиты iptables/ip6tables.
Поскольку в современных дистрибутивах Linux на смену фреймворку iptables пришел nftables, эти утилиты теперь являются просто ссылками на iptables-nft – обертку, которая позволяет нам продолжать использовать старый синтаксис:
sudo iptables -t nat -A OUTPUT -
Наконец, чтобы задать такое же перенаправление с помощью собственного синтаксиса nftables, мы можем воспользоваться утилитой nft и выполнить следующую команду:
sudo nft 'add ru
Перенаправление порта с помощью redir
Альтернативой созданию правила брандмауэра является использование специализированного инструмента, такого как redir, который имеет открытый исходный код и доступен в стандартных репозиториях всех основных дистрибутивов Linux.
На Fedora мы можем установить его следующей командой:
sudo dnf install redir
В дистрибутивах Debian и Debian-based можно использовать:
sudo apt install redir
Redir способен перенаправлять TCP-соединения, приходящие на локальный порт, на определенную комбинацию <адрес>:порт. Часть <адрес> необязательна: если она опущена, то используется адрес 0.0.0.0 (мета-адрес) (он представляет все IPv4-адреса на машине).
Чтобы перенаправить порт 80 на порт 8080 с помощью redir, выполним команду:
sudo redir :80 127.0.0.1:8080
Перенаправления, заданные с помощью redir, не являются постоянными; чтобы они применялись при каждой загрузке системы, необходимо создать сервис, запускающий команду с параметрами -n и -s.
Первая используется для запуска приложения в фоновом режиме, вторая – для перенаправления журналов в syslog.
Практически все дистрибутивы Linux в настоящее время используют Systemd в качестве системы инициализации и менеджера сервисов.
Приведем минимальный пример того, как может выглядеть сервисная единица systemd, запускающая redir при загрузке:
[Unit] Description=Redirect tcp port 80 to 8080 with redir [Service] ExecStart=/bin/redir -sn :80 127.0.0.1:8080 [Install] WantedBy=multi-user.target
Мы можем сохранить блок под именем /etc/systemd/system/redir.service.
Чтобы запустить и включить службу при загрузке, выполним команду:
sudo systemctl enable --now redir.service
Сопоставление непривилегированного порта с контейнером
После того как мы перенаправили трафик порта, мы можем запустить контейнер и привязать его к непривилегированному порту:
docker run --name test_httpd -p 8080:80 httpd:latest
Установка возможности CAP_NET_BIND_SERVICE
Альтернативой перенаправления трафика портов только для docker является установка возможности CAP_NET_BIND_SERVICE для бинарного файла /usr/bin/rootlesskit.
Возможности – это специальные разрешения, которые могут быть назначены процессам в Linux.
В данном случае нас интересует именно CAP_NET_BIND_SERVICE, поскольку она позволяет привязать сокет к привилегированному порту.
Для назначения возможности CAP_NET_BIND_SERVICE двоичному файлу rootlesskit выполним следующую команду:
sudo setcap cap_net_bind_service=ep /usr/bin/rootlesskit
Чтобы удалить эту возможность:
sudo setcap -r /usr/bin/rootlesskit
Изменение диапазона непривилегированного порта
Последняя стратегия, которую мы здесь рассмотрим, заключается в изменении диапазона привилегированных портов.
Чтобы проверить, какие порты система считает привилегированными, достаточно прочитать файл /proc/sys/net/ipv4/ip_unprivileged_port_start: он содержит первый непривилегированный порт в системе (по умолчанию 1024).
Продолжая предыдущий пример, для того чтобы система считала порт 80 непривилегированным, запишем в этот файл соответствующее значение:
echo 80 | sudo tee /proc/sys/net/ipv4/ip_unprivileged_port_start
Чтобы сделать это изменение постоянным, необходимо добавить следующую строку в конфигурационный файл в каталоге /etc/sysctl.d/:
echo net.ipv4.ip_unprivileged_port_start = 80 | sudo tee /etc/sysctl.d/90-unprivileged_port_start.conf
Обратите внимание, что при использовании этой стратегии система будет рассматривать все порты >= 80 как непривилегированные!
Заключение
В этом руководсте мы рассмотрели некоторые методы, которые можно использовать для того, чтобы разрешить контейнеру без корня привязываться к привилегированному порту.
Мы рассмотрели, как перенаправить трафик с привилегированного порта на непривилегированный, как установить характеристику CAP_NET_BIND_SERVICE, и, наконец, как изменить диапазон привилегированных портов.
