Rootless Docker: стоит ли переходить

Что даёт rootless Docker по безопасности, где он удобен и какие ограничения приносит в реальной эксплуатации — и при чём здесь Podman

Rootless Docker звучит как очевидное улучшение безопасности: если демон и контейнеры работают без root, значит риск ниже. Но как только дело доходит до реального использования, выясняется, что вместе с новым уровнем изоляции приходят ограничения по сети, storage, cgroups и привычным сценариям администрирования.

Эта статья — практический разбор: что именно меняется в модели безопасности, где rootless режим действительно помогает, а где неожиданно усложняет жизнь. И заодно ответ на вопрос, который возникает почти сразу: если нужен rootless — может, сразу взять Podman?

Rootless Docker: демон и контейнеры работают от обычного пользователя через user namespaces

В статье

Что такое rootless Docker

В обычной (rootful) установке Docker daemon (dockerd) работает от root. Это удобно — ему доступно всё: сеть, storage-драйверы, cgroups, монтирование. Но и цена высока: кто получил доступ к Docker-сокету, фактически получил root на хосте (docker run -v /:/host ... — и корень файловой системы у вас в контейнере).

Rootless Docker запускает и сам демон, и контейнеры от имени обычного непривилегированного пользователя. Ключевой механизм — user namespaces: внутри контейнера процесс видит себя как root (uid 0), но снаружи, на хосте, это обычный пользователь, а uid процессов контейнера отображаются в выделенный диапазон subordinate uid/gid из /etc/subuid и /etc/subgid.

flowchart TB subgraph Rootful["Rootful Docker"] RD["dockerd (root)"] --> RC["контейнер: root = root хоста"] end subgraph Rootless["Rootless Docker"] UD["dockerd (user 1000)"] --> UC["контейнер: root внутри\n= user 1000 снаружи"] end style Rootful fill:#f3dcdc,stroke:#a05b5b style Rootless fill:#dcecd8,stroke:#5b8a5e style RD fill:#e8c8c8,stroke:#a05b5b style RC fill:#e8c8c8,stroke:#a05b5b style UD fill:#c9e4c5,stroke:#5b8a5e style UC fill:#c9e4c5,stroke:#5b8a5e

flowchart TB
    subgraph Rootful["Rootful Docker"]
        RD["dockerd (root)"] --> RC["контейнер: root = root хоста"]
    end
    subgraph Rootless["Rootless Docker"]
        UD["dockerd (user 1000)"] --> UC["контейнер: root внутри\n= user 1000 снаружи"]
    end

    style Rootful fill:#f3dcdc,stroke:#a05b5b
    style Rootless fill:#dcecd8,stroke:#5b8a5e
    style RD fill:#e8c8c8,stroke:#a05b5b
    style RC fill:#e8c8c8,stroke:#a05b5b
    style UD fill:#c9e4c5,stroke:#5b8a5e
    style UC fill:#c9e4c5,stroke:#5b8a5e

Демон слушает не системный /var/run/docker.sock, а пользовательский сокет в $XDG_RUNTIME_DIR/docker.sock. Каждый пользователь может иметь свой изолированный экземпляр Docker.

Важно не путать два разных «root»:

  • rootless Docker — без root работает сам демон;
  • запуск контейнера без root внутри (USER в Dockerfile, --user) — это про процесс внутри контейнера и работает в любой установке.

Это ортогональные вещи. Rootless Docker про то, от кого работает демон; USER appuser — хорошая практика независимо от того, rootful у вас Docker или rootless.

Что меняется в модели безопасности

Главная ценность rootless — сокращение blast radius при компрометации демона или сокета:

  • уязвимость в dockerd или утечка доступа к сокету больше не означает мгновенный root на хосте — атакующий ограничен правами обычного пользователя и его subuid-диапазоном;
  • побег из контейнера (container escape) упирается в те же ограничения: «root» сбежавшего процесса на хосте — это непривилегированный uid;
  • на shared-хостах несколько пользователей могут запускать контейнеры, не мешая друг другу и не получая привилегий над системой.

Чего rootless не делает:

  • это не замена остальной гигиене — seccomp, AppArmor/SELinux, --cap-drop, read-only rootfs по-прежнему нужны;
  • это не защита от уязвимостей в самом приложении внутри контейнера;
  • user namespace mapping — сильная, но не абсолютная граница; ядро всё равно общее.

Иными словами, rootless поднимает планку для атакующего, целящегося в хост через Docker, но не отменяет defense-in-depth.

Что ломается или усложняется

Здесь начинается реальная эксплуатация. Большинство ограничений — следствие того, что у непривилегированного процесса просто нет части возможностей.

Привилегированные порты (<1024). Обычный пользователь не может забиндить порт ниже 1024. -p 80:80 из коробки не сработает. Лечится либо публикацией на высокий порт (-p 8080:80), либо разрешением на хосте:

# единоразово
sudo sysctl net.ipv4.ip_unprivileged_port_start=80
# постоянно — в /etc/sysctl.d/
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-rootless.conf

Сеть. Исходящий трафик контейнеров в rootless идёт через пользовательский сетевой стек — slirp4netns (раньше) или pasta (современный, быстрее и с сохранением исходного IP). Это даёт небольшой оверхед и иногда сюрпризы: по умолчанию контейнер видит не реальный source IP клиента, а адрес прокси. ICMP/ping внутри контейнера может требовать дополнительной настройки net.ipv4.ping_group_range.

Storage. Драйвер overlay2 в rootless работает нативно только на свежих ядрах (≈5.11+). На старых придётся использовать fuse-overlayfs — он медленнее и даёт другие характеристики по производительности I/O. Для нагруженных сборок это заметно.

cgroups и лимиты ресурсов. Управление --cpus, --memory требует cgroups v2 с делегированием через systemd. Без правильной delegation часть лимитов будет молча игнорироваться — особенно болезненно, если вы рассчитываете на них в production.

Прочее. Не все фичи доступны: некоторые сценарии с --privileged, отдельные варианты монтирования, часть сетевых режимов. Перед миграцией стоит проверить именно ваши сценарии, а не доверять «обычно работает».

Установка и smoke test

Rootless ставится поверх обычного пакета Docker отдельным скриптом — от имени целевого пользователя, без sudo:

# пакеты-зависимости: uidmap (newuidmap/newgidmap), dbus для user-сессии
sudo apt-get install -y uidmap dbus-user-session
# если ставили Docker из apt-репозитория Docker — нужен ещё пакет с rootless-скриптом
sudo apt-get install -y docker-ce-rootless-extras

# установка rootless-режима от обычного пользователя
dockerd-rootless-setuptool.sh install

# подсказанные переменные окружения — в ~/.bashrc
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

Предусловие, о котором легко забыть: для текущего пользователя в /etc/subuid и /etc/subgid должно быть выделено не менее 65 536 subordinate uid/gid (официальное требование Docker). Обычно пакеты настраивают это автоматически, но на кастомных образах диапазон стоит проверить — без него dockerd-rootless-setuptool.sh откажется работать.

Чтобы демон жил без активной сессии (после выхода по SSH), включаем linger и автозапуск через user-systemd:

sudo loginctl enable-linger $(whoami)
systemctl --user enable --now docker

Smoke test — убедиться, что демон действительно непривилегированный:

docker info | grep -i rootless          # rootless: true
docker run --rm alpine id               # uid=0(root) ВНУТРИ контейнера
ps -o user= -p $(pgrep -n dockerd)      # dockerd работает от вашего пользователя, не root

Живой стенд, который наглядно показывает разницу rootful/rootless на владельце файлов в volume и на ограничении портов, — в digital-cookbook: docker/rootless. Поднимается одной командой (bash scripts/smoke.sh).

Rootless и Compose в production

Docker Compose с rootless работает — он общается с тем же пользовательским сокетом через DOCKER_HOST. Но есть нюансы, которые важны именно для production:

  • порты — все ports: в compose.yaml подчиняются правилу <1024, ревизуйте маппинги (или ip_unprivileged_port_start);
  • bind mounts — каталоги с хоста монтируются с учётом uid-маппинга; файлы, созданные контейнером, на хосте будут принадлежать subuid, а не вашему пользователю напрямую. Подробнее о владельцах и правах — в статье про volumes и permissions;
  • лимиты ресурсовdeploy.resources / mem_limit сработают только при корректной cgroups v2 delegation;
  • автозапускrestart: always опирается на user-systemd-сервис docker; без enable-linger контейнеры остановятся при выходе пользователя.

Если вы уже выстроили production-Compose по этому материалу, переход на rootless — это в основном ревизия портов, томов и лимитов, а не переписывание с нуля.

Docker rootless vs Podman

Закономерный вопрос: если так хочется rootless — зачем городить его поверх Docker, когда есть Podman, у которого rootless заложен в дизайн?

Ключевая разница — архитектура:

  • Docker — клиент-серверный: есть постоянный демон dockerd, контейнеры — его дочерние процессы. Rootless здесь надстройка над демонной моделью.
  • Podman — daemonless: нет постоянного демона, каждый контейнер запускается как обычный дочерний процесс (fork-exec) под управлением conmon. Rootless — режим по умолчанию, а не опция.
Docker rootless Podman
Демон есть (rootless dockerd) нет (daemonless)
Rootless надстройка по умолчанию
CLI docker podman (CLI-совместим)
Compose Docker Compose podman compose / docker-compose через сокет
systemd-интеграция user-сервис демона нативная (Quadlet, генерация unit’ов)
Pods (как в K8s) нет есть

Практический ориентир:

  • остаётесь на Docker rootless, если у вас уже выстроена экосистема вокруг Docker (Compose-файлы, CI, привычки команды), а rootless нужен точечно для снижения рисков;
  • смотрите на Podman, если затеваете окружение с нуля, цените отсутствие демона, планируете тесную интеграцию с systemd или думаете в сторону Kubernetes (pods, генерация манифестов).

Podman не «лучше» или «хуже» — он про другую модель. Для темы rootless он важен как напоминание: иногда правильный ответ на «как сделать Docker безопаснее» — это «а нужен ли здесь именно Docker».

Где rootless оправдан, а где остаться на rootful

Rootless имеет смысл:

  • домашний lab и dev-окружения, где безопасность хоста важнее предельной производительности;
  • user-space окружения и CI-раннеры, где не хочется давать сборкам root;
  • отдельные сервисные хосты с повышенным вниманием к изоляции и multi-tenant сценарии.

Лучше остаться на rootful:

  • нужен максимум совместимости и все фичи (сложная сеть, --privileged, специфичные mount’ы);
  • инфраструктура сильно завязана на привычные сетевые сценарии и реальный source IP без танцев с pasta;
  • команда пока не готова разбираться с subuid, cgroups delegation и storage-нюансами — поспешный переход добавит инцидентов, а не безопасности.

Checklist: стоит ли вам переходить

  1. Что вы защищаете — хост от компрометации демона/сокета? Если да, rootless бьёт в цель.
  2. Нужны ли вам порты <1024 и готовы ли вы настроить ip_unprivileged_port_start?
  3. Достаточно ли свежее ядро для нативного overlay2 (иначе — fuse-overlayfs и просадка I/O)?
  4. Настроена ли cgroups v2 delegation, если вы полагаетесь на лимиты ресурсов?
  5. Проверены ли именно ваши сетевые сценарии (source IP, ping, проброс портов)?
  6. Не проще ли для вашего случая взять Podman вместо rootless-надстройки над Docker?

Если на пункт 1 ответ «не знаю» — сначала проясните модель угроз. Rootless — это инструмент против конкретного риска, а не галочка «стало безопаснее».

Обсуждение в Telegram

Присоединиться →

Комментарии