12k
All articles

Защита Docker на Linux-машине

Доступ к группе Docker дает права root. Защитите Docker в Linux с помощью rootless-режима, правил sudo, Podman, изоляции контейнеров и списка проверок.

OpenReplay Team
OpenReplay Team
Защита Docker на Linux-машине

Добавление пользователя в группу docker даёт ему права root на машине. Это не «почти root», а полноценный root: любой, кто может обращаться к сокету Docker, способен смонтировать файловую систему хоста в контейнер и изменить её.

Вы выполнили usermod -aG docker $USER, чтобы больше не набирать sudo, ведь именно так советует официальное руководство Docker по настройке после установки. Однако на той же странице о настройке Docker Engine в Linux после установки есть предупреждение (в блоке, который большинство читателей пролистывает): членство в этой группе равнозначно правам root. Если вы только осваиваете основы, начните с руководства для начинающих по образам и контейнерам Docker. В этой статье риск демонстрируется одной командой. Затем разбираются способы защиты по степени их эффективности, а в конце приводится чек-лист для аудита машины, доставшейся вам «в наследство».

Ключевые выводы

  • Любой участник группы docker, как и любой процесс, запущенный от его имени, может получить root-оболочку на хосте командой docker run --rm -it -v /:/host alpine chroot /host sh.
  • Rootless Docker запускает и dockerd, и ваши контейнеры внутри пространства имён пользователя (user namespace). Поэтому при компрометации демона или выходе из контейнера атакующий получает непривилегированный UID на хосте, а не root.
  • Суженное правило sudoers для /usr/bin/docker по-прежнему эквивалентно root, но требует ввода пароля перед использованием Docker и журналирует каждый вызов.
  • Podman работает без демона и при запуске от имени обычного пользователя по умолчанию использует rootless-режим. Его CLI в основном совместим с Docker, поэтому alias docker=podman покрывает большинство повседневных сценариев.
  • На унаследованном хосте проверьте, кто входит в группу docker, от имени какого пользователя работает dockerd, какие права установлены на сокет и не слушает ли демон TCP-порт 2375.

Почему группа docker даёт права root?

Группа docker эквивалентна root, потому что демон Docker работает от root, а группа определяет право записи в его Unix-сокет. Возможность писать в этот сокет означает возможность приказать процессу с правами root сделать что угодно. Если вас интересуют внутренние механизмы, руководство по правам доступа к файлам в Linux объясняет, как групповые права файла определяют, кто может его открыть. Для Docker важна лишь одна деталь: сокет доступен на запись группе docker.

Вот доказательство:

docker run --rm -it -v /:/host alpine chroot /host sh

Эта команда выполняет bind-монтирование корня / хоста в контейнер, а затем делает в него chroot. Теперь у вас есть root-оболочка в реальной файловой системе хоста. Отсюда можно отредактировать /etc/shadow или /etc/sudoers либо добавить SSH-ключи в /root. Не нужны ни пароль, ни эксплойт. То же самое может сделать любой скрипт, зависимость или скомпрометированное расширение редактора, работающие от имени вашего пользователя. В документации Docker это описано в разделе «Поверхность атаки демона Docker».

Настройка rootless Docker

Rootless Docker — это режим, в котором dockerd и все контейнеры работают внутри пространства имён пользователя, принадлежащего вашей учётной записи. Именно поэтому он напрямую устраняет проблему группы docker. Сокет располагается по пути $XDG_RUNTIME_DIR/docker.sock, а не /var/run/docker.sock. Пространство имён создаёт RootlessKit. Вспомогательные setuid-утилиты newuidmap и newgidmap сопоставляют UID 0 в пространстве имён с вашим собственным UID, а остальные UID контейнера — с диапазоном из /etc/subuid и /etc/subgid. Сеть обеспечивается стеком пользовательского режима, например slirp4netns, gvisor-tap-vsock или pasta.

Установите необходимые пакеты и проверьте диапазоны подчинённых идентификаторов (subordinate IDs). Документация по rootless-режиму требует, чтобы для вашего пользователя и в /etc/subuid, и в /etc/subgid был выделен диапазон не менее чем из 65 536 подчинённых идентификаторов.

sudo apt-get install -y uidmap docker-ce-rootless-extras
grep ^$(whoami): /etc/subuid /etc/subgid

Отключите root-демон, затем запустите утилиту настройки от имени обычного пользователя:

sudo systemctl disable --now docker.service docker.socket
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger $USER
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

Строка с enable-linger сохраняет работу демона после выхода из системы. Когда rootless Docker заработает, удалите себя из старой группы командой sudo gpasswd -d $USER docker. Инструкции для других дистрибутивов приведены в официальной документации по rootless-режиму. Если после этого клиент не может подключиться к демону, обратитесь к руководству по устранению ошибки «Docker daemon not running»: в нём разобраны проверки сокета и systemd.

Для rootless-режима необходимы непривилегированные пространства имён пользователя. В Ubuntu 24.04 и более новых версиях они по умолчанию ограничены через AppArmor. Пакет apparmor в Ubuntu уже содержит профиль, необходимый для rootlesskit, поэтому установка docker-ce-rootless-extras через apt работает без дополнительных действий. Если же вы устанавливаете Docker скриптом get.docker.com/rootless, профиль придётся добавить вручную. Не отключайте это ограничение для всей системы с помощью kernel.apparmor_restrict_unprivileged_userns=0. Прочие сбои описаны на странице устранения неполадок rootless-режима.

Чем приходится платить за rootless-режим?

Основные компромиссы по сравнению с демоном, работающим от root (rootful):

ОграничениеОбходное решение
По умолчанию нельзя привязаться к портам ниже 1024sysctl net.ipv4.ip_unprivileged_port_start=80 или обратный прокси
--privileged не даёт реальных привилегий на хостеДействуют только capabilities внутри пространства имён пользователя
Сеть в пользовательском режиме медленнее драйвера bridgeЭкспериментальный драйвер lxc-user-nic или нагрузочное тестирование перед выводом в продакшен
Ядрам версий до 5.11 требуется fuse-overlayfsЯдро 5.11+ нативно поддерживает overlay2

На странице устранения неполадок перечислены и другие ограничения, например отсутствие поддержки AppArmor, контрольных точек (checkpoints) и overlay-сетей.

Использование sudo с суженным правилом

Если rootless-режим ломает вашу рабочую нагрузку, компромиссный вариант — требовать sudo для Docker вместо членства в группе docker. Этот способ по-прежнему эквивалентен root, ведь через sudo docker можно выполнить ту же команду с chroot. Зато при каждом вызове вы получаете запрос пароля и запись в журнале аудита. Код, незаметно выполняющийся от имени вашего пользователя, больше не имеет доступа к сокету.

sudo gpasswd -d alice docker
sudo visudo -f /etc/sudoers.d/docker
alice ALL=(root) /usr/bin/docker

Это правило разрешает запуск только бинарного файла Docker, а не произвольных команд. Историю использования можно просмотреть командой journalctl _COMM=sudo. В документации по настройке после установки также описан метод с newgrp: доступ к Docker защищается паролем группы, а CLI при этом по-прежнему работает от имени вашего пользователя.

Переход на Podman

Podman не использует демон и при запуске от имени обычного пользователя по умолчанию работает в rootless-режиме. Контейнеры выполняются как дочерние процессы вашего пользователя внутри пространства имён пользователя с теми же сопоставлениями из /etc/subuid. Его CLI настолько близок к Docker, что большинство команд работает без изменений.

sudo apt-get install -y podman
alias docker=podman
podman run --rm -p 8080:80 docker.io/library/nginx

Различия проявляются в частностях: полностью квалифицированные имена образов, поддержка Compose и инструменты, которые рассчитывают на наличие сокета Docker. Протестируйте свои скрипты, прежде чем переводить на Podman всю команду.

Усиление защиты самого контейнера

Какой бы демон вы ни использовали, ограничьте возможности процесса внутри контейнера. Большую часть рисков закрывают четыре настройки: непривилегированный пользователь в USER, отсутствие capabilities сверх необходимых приложению, корневая файловая система в режиме только для чтения и запрет повышения привилегий через setuid-бинарники.

FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
docker run -d \
  --cap-drop=ALL \
  --read-only --tmpfs /tmp \
  --security-opt no-new-privileges \
  -p 8080:8080 myapp

Инструкция USER в Dockerfile задаёт UID, с которым выполняется процесс. --cap-drop=ALL удаляет все capabilities. Если приложению действительно нужна какая-то из них, верните её через --cap-add. Это приложение слушает порт 8080, поэтому ни одна capability ему не нужна. Сочетание --read-only и --tmpfs означает, что для записи доступен только /tmp. no-new-privileges не позволяет setuid-бинарникам повышать привилегии. Все эти параметры описаны в справочнике по docker run.

Аудит унаследованной машины

На VPS, настроенном кем-то другим, ответьте на четыре вопроса: кто входит в группу docker, от имени какого пользователя работает dockerd, кто может писать в сокет и не слушает ли демон сеть.

getent group docker
ps -eo user,cmd | grep [d]ockerd
ls -l /var/run/docker.sock
sudo ss -ltnp | grep -E ':2375|:2376'
systemctl cat docker | grep -- '-H'
grep -s hosts /etc/docker/daemon.json

Каждый пользователь, которого выводит getent, имеет root-доступ. Если ps показывает root, демон работает в rootful-режиме. Права на сокет должны выглядеть как srw-rw---- root docker. Любой бит записи для всех (world-writable) требует немедленного исправления. Прослушивание порта 2375 означает, что API Docker открыт без TLS. Актуальные версии Docker Engine с такой конфигурацией не запускаются, но на унаследованном хосте может работать более старая версия. Любой, кто может достучаться до этого порта, получает root. TLS-слушатель на порту 2376 безопаснее, но root по-прежнему получает любой владелец клиентских ключей. Воспользуйтесь руководством по защите сокета демона, чтобы перейти на SSH или взаимный TLS (mTLS). Также проверьте контейнеры, в которые смонтирован сокет: каждый из них даёт доступ к root.

docker ps -q | xargs -r docker inspect \
  --format '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock

Доступ к сокету Docker — это root-доступ, и точка. Начните с запуска getent group docker на каждой машине, с которой работаете. Затем либо переведите этих пользователей на rootless Docker или Podman, либо закройте сокет журналируемым правилом sudo.

Часто задаваемые вопросы

В чём разница между rootless Docker и userns-remap?

При использовании userns-remap dockerd по-прежнему работает от root, и только контейнеры выполняются под переназначенными подчинёнными UID. Пример — пользователь dockremap, который создаётся, если задать для userns-remap значение 'default' в /etc/docker/daemon.json. Такой подход не закрывает брешь, связанную с группой docker: участник группы может передать --userns=host, чтобы отключить переназначение для отдельного контейнера, и выполнить выход через chroot. В rootless-режиме от имени вашего пользователя работает сам демон.

Можно ли запускать rootless и rootful Docker на одной машине?

Да. Два демона используют разные сокеты: /var/run/docker.sock для rootful и $XDG_RUNTIME_DIR/docker.sock для rootless. Команда dockerd-rootless-setuptool.sh install создаёт контекст CLI с именем 'rootless' и переключается на него. Используйте docker context use default, чтобы обращаться к root-демону, и docker context use rootless, чтобы переключиться обратно. Пока rootful-демон продолжает работать, любой, кто может писать в его сокет, по-прежнему имеет права root.

Переносятся ли существующие образы и контейнеры в rootless Docker?

Нет. Rootless-демон хранит данные в собственном каталоге (data root), по умолчанию ~/.local/share/docker. Он отделён от каталога /var/lib/docker, который использует root-демон. Образы, контейнеры и тома, созданные в rootful Docker, rootless-демону не видны. Загрузите образы заново либо экспортируйте их командой docker save из rootful-демона и импортируйте командой docker load в rootless-демон. Обновите задания резервного копирования, которые охватывают только /var/lib/docker.

Почему у файлов, записанных rootless-контейнерами, на хосте странные владельцы?

Rootless Docker сопоставляет UID контейнера с UID хоста. Root внутри контейнера соответствует вашему UID, поэтому файлы, которые он записывает в bind mount, принадлежат вам. Все остальные UID контейнера отображаются в ваш диапазон из /etc/subuid. Если диапазон начинается со 100000, UID 1000 в контейнере становится UID 100999 на хосте, и ваш пользователь не может редактировать такие файлы напрямую. Выполнив chown 0:0 внутри контейнера, вы вернёте файлы своему пользователю.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.