Как нестандартная конфигурация Docker в новом дистрибутиве открыла путь к root
Свежая уязвимость в одном из дистрибутивов Linux возникла не из-за ошибки в коде ядра, а из-за настроек Docker, которые установщик применил по умолчанию — и никого об этом не предупредил. Итог: любой процесс в системе мог подняться до root — без пароля, без sudo, без единого запроса к пользователю.
Механика простая. Демон Docker работает под root и слушает сокет, а клиент вроде docker-cli шлёт туда запросы. Кто получил доступ к сокету — получил и root. Проверяется это в две команды. Обычный пользователь не может заглянуть в /etc/sudoers.d/:
$ ls /etc/sudoers.d/
ls: cannot open directory '/etc/sudoers.d/': Permission deniedА теперь тот же просмотр, но через контейнер:
$ docker run -v /:/host alpine:latest ls /host/etc/sudoers.d/
90-cloud-init-users
READMEКоманда монтирует корень хоста в /host внутри Alpine, и контейнер, запущенный от имени демона, читает всё, что заблокировано для вашей учётной записи. Пароль не спрашивали — его просто негде было спросить.
Чуть более изобретательный скрипт тем же приёмом перепишет системные файлы, поставит задачу в cron или вытащит данные наружу — и вы этого не заметите.
Стоит уточнить: на голой установке Docker такой трюк не пройдёт, потому что docker доступен только root, а обычному пользователю пришлось бы каждый раз вводить sudo docker. Уязвимость появляется там, где доступ к сокету ослабили ради удобства — например, добавили себя в группу docker, чтобы не набирать sudo. Ровно это и сделал установщик того дистрибутива. И ровно это описано в официальной документации Docker как штатный способ работы без sudo. Если вы пользуетесь Docker на Linux, скорее всего, вы эту настройку уже применили — а значит, ваша система открыта для повышения привилегий.
Архитектурная причина: демон под root и доступ к сокету
Клиент-серверная архитектура Docker была выбрана с самого начала — и именно она породила проблему, которую невозможно «залатать» настройками отдельного контейнера. На обычной установке демон Docker работает в фоне от имени пользователя root и предоставляет весь функционал через API. Клиенты вроде docker-cli отправляют запросы в этот API через сокет, который демон слушает.
Из этого следует простой, но неприятный вывод: тот, кто получил доступ к демону, получил возможность выполнять код с правами root. Это не гипотетическая уязвимость для избранных — это следствие архитектурного решения.
Пока доступ к docker есть только у root, эскалация привилегий не имеет смысла: работать с Docker из-под обычной учётной записи всё равно не выйдет без sudo. Но на практике так почти никто не живёт. Если вы запускаете docker от своего пользователя, значит, права на сокет были расширены — например, добавлением в группу docker. Это удобно ровно настолько, насколько опасно.
Официальная документация Docker описывает, как настроить работу без sudo, — и это отличная иллюстрация того, насколько неудобен вариант «только root». Цена удобства — система, уязвимая к эскалации привилегий.
Стоит оговориться про другие платформы. Docker работает только на Linux, поэтому Docker Desktop для macOS и Windows создаёт Linux-виртуальную машину. Дополнительный слой изоляции заметно снижает риски для физического хоста. WSL — отдельный случай: здесь Linux-ядро работает прямо в хосте, и риски сопоставимы с обычной Linux-системой.
Ловушка группы docker и иллюзия удобства без sudo
Одна команда usermod -aG docker $USER — и sudo больше не нужен. Удобно? Да. Безопасно? Нет, и вот почему.
Официальные инструкции Docker действительно описывают этот шаг: чтобы работать с docker от обычной учётной записи, вы добавляете себя в группу docker, и sudo docker превращается в просто docker. Но что именно вы получаете вместе с удобством? Членство в этой группе означает доступ к сокету демона — а демон работает от root. Значит, каждый, кто попал в группу, может попросить демон выполнить произвольный код с правами root. Никакого пароля, никакого sudo, никакого запроса на подтверждение.
Проверка занимает секунды. Обычный пользователь так не может:
$ ls /etc/sudoers.d/
ls: cannot open directory '/etc/sudoers.d/': Permission deniedА через контейнер — может, потому что запрос уходит к демону:
$ docker run -v /:/host alpine:latest ls /host/etc/sudoers.d/
90-cloud-init-users
READMEЧто здесь произошло: docker run поднял контейнер Alpine, смонтировав корень хост-системы в каталог /host. Контейнер исполняется под root через демон, поэтому видит все файлы — ограничения вашей учётной записи на него не действуют. Пароль не спрашивали, аутентификация не потребовалась, команда просто сработала. Чуть более изобретательный скрипт тем же приёмом изменит системные файлы, добавит задание в cron или выгрузит данные наружу — и вы этого не заметите.
И вот ключевое заблуждение. Добавление в группу не «повышает» ваши привилегии и не «понижает» права демона — оно лишь убирает барьер между вами и уже существующим всемогущим API. Раньше эскалация упиралась в пароль sudo; теперь барьера нет. Опасность не исчезла — она перестала быть заметной, потому что исчезло напоминание в виде запроса пароля.
Именно так и произошёл недавний инцидент в одном из новых дистрибутивов Linux: установщик по умолчанию ослабил доступ к сокету Docker, не предупредив пользователей. Результат — любой процесс в системе мог получить root без пароля и без sudo. Никакого взлома «для гениев»: конфигурация по умолчанию сделала это рядовой операцией.
Отсюда простой критерий для самопроверки: состоит ли ваша учётная запись в группе docker? Если да — считайте, что у неё права root, независимо от того, что показывает id. Остаётся вопрос: как убрать сам источник риска, а не его симптом, — об этом дальше.
Где Docker безопаснее: виртуализация, Docker Desktop и WSL
Начнём с неочевидного факта: Docker вообще не существует вне Linux. Поэтому все «нативные» сборки для macOS и Windows — это надстройка над виртуальной машиной с Linux внутри. От того, как эта VM устроена, и зависит, насколько далеко может дотянуться контейнер.
Linux напрямую. Здесь демон Docker работает как фоновый процесс от root и обслуживает запросы через сокет. Если у вас есть доступ к этому сокету, вы просите демон выполнить произвольный код с правами root — и он выполняет, без пароля и подтверждений. Контейнер при этом видит ровно ту файловую систему, которую вы ему смонтировали, вплоть до корня хоста. Именно поэтому дополнительный слой изоляции тут отсутствует: граница между контейнером и хостом проходит только по правам демона. Отсюда простой вывод — чем шире доступ к сокету, тем ближе вы к полному контролю над машиной, и удобство запуска без sudo прямо конвертируется в этот риск.
Docker Desktop на macOS и Windows. Продукт вынужден поднимать Linux-VM — обычно на стандартных средствах виртуализации вроде QEMU или Hyper-V — и уже внутри неё крутит Docker. Между вашей рабочей системой и демоном появляется лишний барьер: даже если контейнер получит root внутри гостевой Linux, это root виртуальной машины, а не вашего ноутбука. Риск для физического хоста заметно снижается. Оговорка честная: автор источника не считает себя экспертом по этим решениям и не исключает, что детали могут отличаться от его представления.
WSL. А вот это особый случай, и путать его с предыдущим пунктом не стоит. WSL2 — по сути обычная виртуальная машина Hyper-V, но с более плотной интеграцией в хост: проброс портов, монтирование путей Windows внутрь Linux. Интеграция плотнее — значит, и поверхность соприкосновения с хост-системой шире, чем у аккуратно изолированной VM. Риски становятся сопоставимы с обычным Linux-сервером: если внутри WSL доступ к сокету открыт, эскалация до root вполне может сработать так же, как на голом Linux. Утверждать наверняка автор не берётся, но предлагает дешёвую проверку: потратить пять минут и проверить, проходит ли такой сценарий в вашей конфигурации с Docker Desktop или Podman Desktop.
Практическая развязка простая. На Linux и в WSL граница риска лежит ровно там, где лежит доступ к сокету, — и именно там стоит наводить порядок. На macOS и Windows дополнительный слой виртуализации сам по себе работает как буфер, но не как гарантия: он снижает последствия, а не отменяет их, особенно когда интеграция с хостом тесная.
Rootless Docker: пользовательский демон как половинчатое решение
Официальная инструкция по rootless-режиму Docker начинается с неожиданного шага: сначала нужно установить Docker обычным способом, а потом вручную отключить системный демон. Логика такая — вы ставите привычный пакет, убеждаетесь, что он на месте, а затем сознательно гасите ту службу, которая раньше принимала все запросы.
Ключевое отличие режима в том, кто владеет демоном. В стандартной установке фоновая служба работает от root и слушает сокет, поэтому любой, кто получил доступ к сокету, может попросить демон выполнить произвольную команду от имени root. В rootless-варианте вы вместо системной службы запускаете пользовательскую замену демона — она работает от вашей учётной записи и обслуживает только вас.
Установка выглядит так: после отключения системного демона вы запускаете предоставляемый разработчиками скрипт, который создаёт пользовательский демон для текущего пользователя. С этого момента контейнеры стартуют под вашим аккаунтом, а не под root, и путь к повышению привилегий через сокет закрывается. Автор статьи честно называет это решение «немного хакерским» (a bit hacky) именно из-за ручной возни: обычная установка, ручное отключение, отдельный скрипт для пользовательского демона.
Если сравнить с Podman, разница в объёме ручной работы заметна сразу. Podman — бездемонная платформа: вы просто устанавливаете его и работаете, никаких фоновых служб поднимать не нужно, и все контейнеры идут под вашим пользователем без реального пути к эскалации до root. Именно поэтому автор рекомендует присмотреться к Podman, если вы готовы иногда набирать podman вместо docker (или добавить alias docker=podman в конфиг оболочки) — большинство привычных сценариев при этом не меняется.
Практический нюанс при миграции: Podman не всегда знает, откуда тянуть образ, тогда как Docker по умолчанию использует Docker Hub. Команда docker pull postgres в Podman может не сработать, и тогда образ указывают полным адресом: podman pull docker.io/library/postgres. Это мелкая, но регулярная правка в привычках.
Ещё два следствия бездемонной модели, о которых стоит знать заранее. Без демона контейнеры не перезапускаются после перезагрузки, поэтому для серверов, работающих круглосуточно, нужна дополнительная настройка — интеграция с systemd через группу команд podman quadlet, устанавливающая контейнеры как отдельные службы. И если приложению нужен API для запуска контейнеров, Podman предоставляет опциональную службу podman system service, которая работает только для запустившего её пользователя и отдаёт API, совместимый с Docker.
Podman: бездемонный подход и запуск контейнеров от своего пользователя
Установка Podman не требует никаких предварительных манипуляций: вы ставите пакет и сразу получаете рабочую систему контейнеров. Никаких фоновых служб — ни системных, ни пользовательских. Контейнеры запускаются напрямую от вашей учётной записи, и весь стек работает через podman-команду, которая по большей части совпадает с привычным синтаксисом Docker. Если добавить alias docker=podman в конфигурацию оболочки, значительная часть повседневных сценариев продолжит работать без изменений — но уже под вашим пользователем, без пути к повышению привилегий до root.
Почему это принципиально меняет модель угроз? Вспомните демонстрацию из предыдущей секции: команда docker run подключала файловую систему хоста в /host и читала /etc/sudoers.d/ без пароля — именно потому, что запрос обслуживал root-демон. У Podman такого звена просто нет. Контейнер запускается от того же пользователя, что и вы, и не может получить доступ к файлам, которые недоступны вашей учётной записи. Атакующему, получившему доступ к сокету Podman, нечего эскалировать — он уже ограничен правами пользователя.
Именно эта разница и делает Podman не «косметическим» форком, а архитектурной альтернативой. Rootless-режим Docker, как отмечает автор оригинальной статьи, всё равно вынужден имитировать пользовательский демон поверх системного. Podman же просто не нуждается в демоне — и это преимущество встроено, а не надстроено.
Что ломается при переходе на Podman: реестры, перезапуск и совместимость
Начнём с реестра. Docker по умолчанию тянет образы из Docker Hub, а Podman — не всегда: результат зависит от его конфигурации. Команда docker pull postgres под Podman может просто не сработать, потому что платформа не знает, откуда брать образ. Автор рекомендует приучить себя указывать полное имя образа: podman pull docker.io/library/postgres. Настроить Docker Hub как реестр по умолчанию тоже можно, но привычка писать полный адрес избавляет от сюрпризов.
Второе отличие вытекает из бездемонности. Без фоновой службы Podman не умеет перезапускать контейнеры после перезагрузки хоста — а для серверов, которые должны работать круглосуточно, это критично. Решается через интеграцию с systemd: группа команд podman quadlet устанавливает контейнеры как отдельные юниты-сервисы.
Третье — API. Многие приложения запускают контейнеры и общаются с ними не через CLI, а по программному интерфейсу, и в Docker это распространённый сценарий. Podman закрывает его опциональной службой: команда podman system service поднимает API, совместимый с Docker-овским. Нужно передать адрес сокета и, при желании, время жизни службы. Служба работает только для запустившего её пользователя; на серверах её можно держать постоянно через systemd.
Наконец, рабочие процессы. Прозрачно переносятся не все — автор пишет об этом прямо, уточняя, что совпадает «подавляющее большинство» сценариев, но не все. Если вы полагаетесь на инструменты вокруг Docker API, есть обходной путь: запустить API-службу Podman, указать в переменной DOCKER_HOST путь к сокету Podman — и утилиты, работающие с Docker API, продолжат работать, поскольку интерфейсы совместимы. Альтернатива — постепенно переходить на родные инструменты Podman, включая Podman Compose; правда, по отзывам в комментариях, он бывает менее предсказуем, чем написанные вручную bash-скрипты, особенно на сложных конфигурациях.
Как перейти на rootless-контейнеры: systemd, API и практический совет
Сравнение двух путей отказа от root-демона даёт три вывода. Первый: Docker в rootless-режиме — это скорее костыль, чем решение. Официальная инструкция требует сначала поставить обычный Docker, затем вручную отключить системный демон и запустить скрипт, который создаёт пользовательскую замену для текущего аккаунта. Podman в этом смысле честнее: поставили — и всё, никаких фоновых служб на хосте не крутится.
Второй: отказ от демона переносит часть привычной функциональности на вас. Без фонового процесса платформа не поднимет контейнеры после перезагрузки, поэтому интеграция с systemd из группы команд podman quadlet — не роскошь, а обязательный элемент для сервера. Третий: совместимость API достижима, но включается осознанно. Сервис запускается командой podman system service с указанием адреса сокета и опционального времени жизни; пока он работает, он действует только от имени запустившего пользователя и предоставляет интерфейс, совместимый с Docker. Именно так подключаются инструменты вроде docker-compose: достаточно выставить переменную DOCKER_HOST на сокет Podman, и они заработают без переписывания.
Практический совет: не смешивайте два способа управления в одной системе. Если API нужен постоянно, заведите пользовательский systemd-юнит для podman system service — тогда сокет поднимается при входе и переживает перезагрузку. Если же API требуется от случая к случаю, запускайте сервис вручную на время работы и управляйте контейнерами нативными командами. Автор, например, прибегает к API лишь изредка, а большую часть времени обходится без него — так вы снижаете число постоянно работающих компонентов и точек отказа.