Миф: контейнер — это песочница, из него не выйти
«У нас всё в Docker, так что даже если приложение взломают — атакующий окажется в изолированной песочнице и дальше не пройдёт» — фраза, которую можно услышать почти в любой команде, где есть хотя бы один docker-compose.yml. Проблема в том, что это наполовину правда, а наполовину опасное заблуждение: контейнер действительно изолирует процесс, но далеко не так надёжно, как думают, и конкретные настройки конфигурации решают, окажется ли эта изоляция бетонной стеной или картонной перегородкой.
Содержание
Откуда берётся миф и в чём его рациональное зерно
Миф не берётся из ниоткуда — под ним есть реальная техническая база, и именно поэтому в него легко поверить. Docker-контейнер действительно получает собственное изолированное представление о системе за счёт двух механизмов ядра Linux:
- Namespaces (пространства имён) — ядро выдаёт процессу свой набор PID (контейнер не видит процессы хоста), свою сетевую подсистему (свой стек интерфейсов, таблицу маршрутизации), своё дерево точек монтирования, свой hostname (UTS namespace), изолированные IPC-очереди и (в правильно настроенном случае) собственное отображение UID/GID через user namespace.
- Cgroups (control groups) — ограничивают, сколько CPU, памяти, I/O и сетевого трафика процесс может забрать, и не дают одному контейнеру уронить весь хост, съев все ресурсы.
Это реальная и полезная граница. Процесс внутри контейнера действительно не видит /etc/shadow хоста напрямую, не видит чужие процессы через ps aux, не может залезть в соседний контейнер по умолчанию. Для большинства сценариев — запустить недоверенный код разработчика, изолировать друг от друга приложения на одном сервере, ограничить последствия утечки зависимости через npm install — этого достаточно. Если вы арендовали VPS и разносите сервисы по контейнерам вместо того, чтобы валить всё в один голый хост, — это правильный шаг, а не самообман. Подробнее о том, как это работает на уровне ядра, разбирали в статьях про namespaces и про cgroups.
Проблема мифа не в том, что изоляция не работает. Проблема в том, что люди достраивают этот факт до вывода «значит, взлом контейнера не может ничего сделать хосту» — а вот это уже неправда, и именно здесь миф ломается.
Главный технический разлом: одно ядро на всех
Вот ключевое отличие, которое обычно упускают: контейнер — не виртуальная машина. Полноценная ВМ (KVM, VMware, Hyper-V) эмулирует железо и запускает собственное ядро операционной системы поверх гипервизора. У гостевой ВМ нет прямого доступа к ядру хоста вообще — граница между ними проходит на уровне аппаратной виртуализации (Intel VT-x/AMD-V), и чтобы «сбежать» из ВМ на хост, атакующему нужна уязвимость в самом гипервизоре — событие редкое и дорогое.
Контейнер устроен принципиально иначе. Docker, containerd, Podman — это не отдельные ОС, а процессы, которые запускаются на том же самом ядре, что и хост, просто с урезанным набором прав и собственным пространством имён. Если провести аналогию: ВМ — это отдельная квартира с отдельными стенами и отдельным фундаментом, контейнер — это комната с перегородкой в общей квартире. Перегородка реальная, но фундамент, трубы и электропроводка — общие.
Из этого следует прямое практическое последствие: любая уязвимость в самом ядре Linux (эскалация привилегий, use-after-free, баг в подсистеме namespaces) потенциально доступна и из контейнера, потому что контейнер обращается к тому же ядру через те же системные вызовы. Это не гипотетика — это класс уязвимостей, который называется container escape (побег из контейнера), и он регулярно всплывает в бюллетенях безопасности:
- Уязвимости в самом ядре, эксплуатируемые из непривилегированного процесса (эскалация до root на хосте).
- Уязвимости в рантайме контейнеризации — например, известный случай с runc (CVE-2019-5736), когда вредоносный образ мог перезаписать бинарник
runcна хосте и получить выполнение кода с правами root вне контейнера. - Уязвимости в компонентах вроде containerd, когда неправильная обработка путей позволяла выйти за пределы файловой системы контейнера.
Важная оговорка, чтобы не скатиться в панику: такие уязвимости закрываются патчами, и держать рантайм и ядро обновлёнными — это стандартная гигиена, а не экзотика. Проблема не в том, что «Docker дырявый», а в том, что граница изоляции у контейнеров тоньше, чем у ВМ, и полагаться на неё как на абсолют — ошибка.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSТри конфигурационные дыры, которые превращают контейнер в дверь на хост
Уязвимости ядра — это фон, который снижают патчингом. Но большинство реальных побегов из контейнеров происходит не из-за экзотического 0-day, а из-за банальной неправильной конфигурации, которую администратор сам включил, чтобы «всё заработало без плясок с бубном».
Флаг --privileged
docker run --privileged -d myapp
Флаг --privileged отключает почти всю модель безопасности контейнера разом: контейнер получает все Linux capabilities, доступ ко всем устройствам хоста (/dev/*), отключается seccomp-фильтрация системных вызовов, снимаются ограничения AppArmor/SELinux. По сути это способ сказать ядру «отнесись к этому контейнеру почти как к самому хосту». В таком контейнере можно смонтировать блочное устройство хоста, получить доступ к /dev/mem, и в некоторых случаях — напрямую примонтировать корневую файловую систему хоста и выйти за пределы контейнера без каких-либо уязвимостей вообще, просто штатными средствами.
Флаг существует не просто так — иногда он реально нужен (например, для контейнеров, которые сами управляют Docker, для некоторых сценариев с оборудованием). Но использовать его как «быстрое решение, чтобы приложение перестало ругаться на права» — это способ обнулить всю пользу от контейнеризации одним аргументом командной строки.
Смонтированный docker.sock
docker run -v /var/run/docker.sock:/var/run/docker.sock -d some-monitoring-tool
Это одна из самых частых и самых недооценённых дыр. Сокет /var/run/docker.sock — это API самого Docker-демона, который на хосте работает с правами root. Смонтировать его внутрь контейнера — популярная практика для инструментов мониторинга, CI-раннеров, панелей управления (типа Portainer), потому что «так удобнее — контейнер видит остальные контейнеры и может ими управлять».
Проблема в том, что доступ к docker.sock эквивалентен root-доступу на хосте. Через этот сокет процесс внутри контейнера может попросить демон запустить новый контейнер с параметрами --privileged и с примонтированным / хоста внутрь — и это будет штатный, документированный вызов Docker API, а не эксплуатация уязвимости. Разница с обычным контейнером колоссальная: контейнер без доступа к сокету физически не может отдать демону такую команду, а контейнер с сокетом может попросить создать себе соседа с правами бога.
Избыточные capabilities и опасные bind-mount'ы
Даже без --privileged и без сокета есть третий путь — точечно выданные Linux capabilities и монтирование хостовых директорий с широкими правами:
--cap-add=SYS_ADMIN— одна из самых опасных отдельных capability, она открывает доступ к множеству административных операций ядра (монтирование файловых систем, некоторые операции с namespaces) и сама по себе часто используется как ступенька для эскалации.- Монтирование
-v /:/hostили-v /etc:/host-etc«для удобства» — контейнер получает возможность читать и писать в файловую систему хоста напрямую, и в этом случае даже не нужен побег из namespace — доступ уже есть через смонтированный путь. - Запуск процесса внутри контейнера от
root(UID 0) без user namespace remapping — если через одну из дыр выше атакующий всё же выходит за пределы контейнера, он выходит сразу с правами root, а не непривилегированного пользователя.
Разбор случая: как один --privileged уронил весь хост
Опишем типичный (обобщённый по классу реальных инцидентов) сценарий, который повторяется в разных вариациях на тысячах серверов.
Команда разворачивает на VPS веб-приложение с обработкой загружаемых пользователями файлов — конвертация изображений через ImageMagick внутри контейнера. Библиотека ImageMagick исторически имеет длинную историю уязвимостей обработки специально сформированных файлов (класс уязвимостей ImageTragick и последующие). Разработчик один раз столкнулся с ошибкой доступа при попытке контейнера обратиться к USB-сканеру, подключённому для другого сервиса на том же хосте, и вместо того чтобы разобраться с конкретным правом, добавил --privileged, чтобы «эта возня с устройствами наконец заработала» — и забыл об этом.
Дальше — стандартная цепочка:
- Атакующий загружает специально сформированный файл, эксплуатирует уязвимость в библиотеке обработки изображений, получает выполнение произвольного кода внутри контейнера.
- Из-за
--privilegedконтейнер видит все устройства хоста и не ограничен seccomp-фильтром системных вызовов. - Атакующий выполняет
fdisk -lвнутри контейнера и видит блочные устройства хоста (/dev/sdaи подобные) — то, чего в непривилегированном контейнере просто не было бы видно. - Монтирует раздел хоста внутрь контейнера собственными средствами (
mount /dev/sda1 /mnt), получает файловую систему хоста как обычную директорию с правами на запись. - Правит
/mnt/etc/crontabили подкладывает публичный ключ в/mnt/root/.ssh/authorized_keys— и получает постоянный доступ к хосту в обход самого приложения.
С этого момента контейнер, из которого «формально нельзя было выйти», стал прямой дверью на хост — не через хитрую уязвимость ядра, а через один флаг командной строки, поставленный полгода назад ради экономии пяти минут отладки. Постмортем в такой ситуации обычно фиксирует не «продвинутую атаку», а конфигурационный долг: флаг остался в docker-compose.yml, никто не делал ревью прав контейнеров перед продакшеном, мониторинг не отслеживал, какие контейнеры запущены в привилегированном режиме.
Отдельно стоит сказать о случае с docker.sock: если бы вместо --privileged в примере выше был смонтирован сокет демона в контейнер мониторинга рядом, итог был бы тем же самым — просто вместо mount атакующий использовал бы Docker API, чтобы попросить демон создать новый привилегированный контейнер с доступом к / хоста. Оба пути ведут в одну точку: root на хосте.
Практический вывод: изоляция — свойство конфигурации, а не факта контейнеризации
Из разбора выше следует простой, но важный вывод: сам факт «мы запускаем это в Docker» ничего не говорит об уровне защиты. Защищённость конкретного контейнера — это сумма конкретных настроек, а не встроенное свойство контейнеризации как технологии. Практический чек-лист для сервера:
| Настройка | Небезопасно | Безопасно |
|---|---|---|
| Режим запуска | --privileged | Без флага, точечные --cap-add при реальной необходимости |
| Docker socket | Смонтирован внутрь контейнера мониторинга/CI | Не монтируется вовсе, либо только через прокси с ограничением прав (например, docker-socket-proxy) |
| Пользователь в контейнере | root (UID 0) без remap | Non-root пользователь в образе + --userns-remap на демоне |
| Capabilities | Полный набор по умолчанию | --cap-drop=ALL + точечный --cap-add под конкретную нужду |
| Файловая система | Bind-mount / или системных директорий хоста | Именованные volumes под конкретные данные приложения, :ro где возможно |
| Seccomp/AppArmor | Отключены (следствие --privileged) | Профиль по умолчанию активен |
| Рантайм и ядро | Не обновляются месяцами | Регулярные обновления пакетов Docker/containerd и ядра хоста |
Практические команды для проверки текущего состояния на сервере:
# Найти запущенные привилегированные контейнеры
docker ps -q | xargs docker inspect --format '{{.Name}}: Privileged={{.HostConfig.Privileged}}'
# Проверить, не смонтирован ли docker.sock в контейнеры
docker ps -q | xargs docker inspect --format '{{.Name}}: {{.Mounts}}' | grep docker.sock
# Запуск с урезанными capabilities как база по умолчанию
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE -d myapp
Если нужен более строгий периметр, чем даёт стандартный Docker (например, для мультитенантных сценариев, где на одном сервере крутится код разных недоверенных сторон), стоит смотреть в сторону gVisor или Kata Containers — рантаймов, которые добавляют дополнительный слой изоляции поверх обычных контейнеров (перехват системных вызовов или лёгкая ВМ вместо разделяемого ядра), либо действительно переходить на полноценные виртуальные машины для конкретно этого уровня недоверия. Мы уже разбирали смежный вопрос выбора между Docker и LXC и общий свод практик в статье про безопасность Docker — если вы ещё не проходили эти чек-листы по своим серверам, это стоит сделать в первую очередь.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Значит ли это, что Docker вообще не даёт изоляции и его нельзя использовать для безопасности?
Нет. Namespaces и cgroups — реальный и полезный барьер, который останавливает огромный класс проблем (случайные конфликты процессов, утечки ресурсов, базовую изоляцию сервисов друг от друга). Миф не в том, что изоляция «не работает», а в том, что она не абсолютна и не эквивалентна виртуальной машине.
Rootless-режим Docker решает проблему целиком?
Он существенно снижает риск: демон и процессы внутри контейнеров работают от непривилегированного пользователя хоста, поэтому даже успешный побег из контейнера не даёт сразу root на хосте. Это одна из самых эффективных мер, но она не отменяет необходимость следить за capabilities, bind-mount'ами и обновлениями — это дополнительный слой, а не замена остальным.
Как понять, что конкретный контейнер настроен небезопасно, если я не писал docker-compose.yml сам?
Проверьте docker inspect на предмет Privileged: true, посмотрите список Mounts на предмет docker.sock или путей вроде / и /etc с хоста, и CapAdd/CapDrop в секции HostConfig — команды из раздела выше показывают это напрямую.
Виртуальная машина — это всегда более безопасный выбор, чем контейнер?
С точки зрения границы изоляции — да, ВМ жёстче, потому что не делит ядро с хостом. Но это компромисс: ВМ тяжелее по ресурсам и медленнее разворачивается. Для большинства задач на VPS правильный ответ — не «ВМ вместо контейнеров», а «контейнеры с корректной конфигурацией плюс ВМ или отдельный сервер там, где недоверие к коду действительно высокое».
Нужно ли из-за этой статьи срочно пересматривать всю инфраструктуру?
Нет, но стоит один раз пройтись чек-листом по разделу «практический вывод» выше: найти привилегированные контейнеры, найти смонтированные docker.sock, проверить, что процессы внутри образов не работают от root без необходимости. Это разовая ревизия на час-два, а не переделка архитектуры.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →