Трендовые github проекты в нашем телеграм канале. Подпишись → Что меняется, когда демон Docker перестаёт быть root
Классический Docker строится на демоне, который работает от имени суперпользователя. Это удобно: демон свободно создаёт cgroups, сетевые интерфейсы, монтирует файловые системы и управляет пространствами имён без ограничений. Обратная сторона — любая уязвимость в самом Docker, ошибка конфигурации или компрометация контейнера открывает путь к полному захвату хоста. Доступ к сокету /var/run/docker.sock фактически равносилен правам root на всей машине: с ним можно поднять привилегированный контейнер, смонтировать корневую файловую систему хоста или переписать сетевые правила.
Rootless-режим убирает эту зависимость целиком. Флаг --user ограничивает права процесса только внутри отдельного контейнера, а Rootless идёт дальше: весь стек Docker — демон, containerd, runc и управляющие утилиты — исполняется от имени обычного пользователя без единого привилегированного вызова. Скомпрометированный демон в этом случае не даёт злоумышленнику ничего сверх прав того пользователя, из-под которого он запущен.
Два механизма пространств имён, на которых всё держится
В основе Rootless лежит связка user namespaces и network namespace, доступная в ядре Linux без специальных прав.
User namespace решает главную задачу — маппинг идентификаторов. Непривилегированный пользователь на хосте (скажем, с UID 1000) получает внутри своего пространства имён псевдо-root с UID 0, но эти права не выходят за пределы namespace и никак не относятся к реальному root хоста. Диапазоны для маппинга задаются в /etc/subuid и /etc/subgid:
user1:100000:65536
user2:165536:65536
Проверить механизм можно напрямую через unshare:
$ whoami
user1
$ id -u
1000
$ unshare --user --map-root-user
(psroot)$ cat /proc/self/uid_map
0 1000 1
(psroot)$ cat /proc/self/gid_map
0 1000 1
(psroot)$ id -u
0
Процесс внутри unshare видит себя как UID 0, но снаружи это по-прежнему обычный пользователь с UID 1000 — никаких реальных привилегий не выдано.
Со стороны сети всё сложнее: непривилегированный процесс не может создавать классические veth-пары и bridge-интерфейсы, которые использует обычный Docker. Поэтому в Rootless-режиме сетевой стек эмулируется в пользовательском пространстве через slirp4netns — эта утилита поднимает TCP/IP-стек и NAT без единого системного вызова, требующего привилегий. Файловая система контейнеров, в свою очередь, монтируется через fuse-overlayfs вместо стандартного overlay-драйвера ядра, а маппингом UID/GID для новых процессов занимаются newuidmap/newgidmap.
Установка и запуск
Пакет docker-ce-rootless-extras содержит и сам скрипт настройки, и зависимости вроде slirp4netns и fuse-overlayfs:
sudo apt install docker-ce-rootless-extras
dockerd-rootless-setuptool.sh install
Скрипт проверяет, что для текущего пользователя уже настроены диапазоны в /etc/subuid и /etc/subgid, создаёт рабочую директорию ~/.local/share/docker и регистрирует пользовательский systemd-юнит для управления демоном.
Дальше нужно указать клиенту, куда подключаться, и добавить бинарники Rootless Docker в PATH:
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
export PATH=/home/$(whoami)/bin:$PATH
После source ~/.bashrc можно проверить, что демон действительно поднялся в Rootless-режиме:
systemctl --user status docker.service
docker info | grep Rootless # должно быть Rootless: true
Для автозапуска при входе в систему и корректной работы после логаута полезны ещё две команды:
systemctl --user enable docker
sudo loginctl enable-linger $(whoami)
Без enable-linger пользовательские systemd-юниты останавливаются сразу после закрытия последней сессии — для сервера с автономно работающим Docker-демоном это обязательная настройка.
Systemd-юнит и прямой запуск скриптом — не взаимозаменяемы
У Rootless Docker есть два способа запуска, и разница между ними не косметическая. Через dockerd-rootless-setuptool.sh install демон регистрируется как пользовательский systemd-юнит и запускается менеджером сессий — тот корректно формирует всё окружение процесса: создаёт нужные cgroups, подключает pid- и mount-namespace в ожидаемом виде.
Второй способ — прямой запуск через dockerd-rootless.sh — поднимает демон как обычный процесс, минуя systemd. Внешне это выглядит рабочим: контейнеры запускаются, образы собираются. Но окружение оказывается неполным, и это всплывает там, где инструменты полагаются на определённую структуру cgroups или на poll-механизмы systemd. Классический симптом — CI-плагины, которые оборачивают запуск контейнера и потом пытаются получить его состояние через docker top: без корректно созданных cgroups такой вызов не находит нужные данные о процессах контейнера, и пайплайн падает с ошибкой, которая на первый взгляд не похожа на проблему окружения.
Вывод простой: если Rootless Docker разворачивается для автоматизации — CI-агентов, сборочных раннеров, любых процессов, где контейнер запускает контейнер, — запуск через systemd-юнит должен быть выбором по умолчанию, а не «сработает и так».
Rootless внутри Kubernetes
Отдельный случай — развёртывание Rootless Docker внутри пода Kubernetes, например для схемы Docker-in-Docker в динамических сборочных агентах. Здесь придётся явно прокинуть в контейнер поддержку user namespaces и cgroups v2, проверить, что базовый образ включает slirp4netns и fuse-overlayfs, и не забыть про subuid/subgid внутри контейнера — они не наследуются от хост-системы автоматически. Отладка в этом сценарии сложнее вдвойне: если что-то не так с cgroups или сетевым стеком, ошибка проявляется не сразу при старте демона, а на конкретной операции — сборке образа, вызове docker top, попытке пробросить порт.
Практический подход — сначала поднять и проверить Rootless Docker на обычной виртуальной машине по описанной выше схеме, зафиксировать рабочую конфигурацию переменных окружения и systemd-юнита, и только потом переносить её в манифест пода, воспроизводя те же условия внутри контейнера.
Когда Rootless оправдан, а когда нет
Rootless Docker решает конкретную проблему — устраняет root-демон как единую точку компрометации всей системы. Но у него есть цена: производительность сети через slirp4netns ниже, чем у нативных сетевых интерфейсов, часть функциональности вроде некоторых сетевых драйверов и работы с определёнными типами хранилищ ограничена или требует дополнительной настройки, а отладка окружения сложнее из-за дополнительных слоёв абстракции.
Для локальной разработки и тестовых стендов эти ограничения обычно не заметны, а выигрыш в безопасности ощутим сразу — скомпрометированный контейнер не может дотянуться до хоста через сокет демона. В продакшене Rootless стоит рассматривать как дополнительный рубеж обороны, который дополняет уже привычные механизмы — AppArmor, SELinux, seccomp-профили. Итоговое решение — это всегда баланс между удобством, полнотой функциональности и снижением поверхности атаки, и для команд, которые запускают недоверенный или потенциально уязвимый код в контейнерах, этот баланс всё чаще склоняется в сторону Rootless.