Пространства имён Linux: из чего на самом деле сделаны контейнеры

· 3 мин · BadRabbit

Каждый воркер Евы запускается в песочнице bwrap. Пора разобраться, что именно она отделяет, и заодно проверить, правда ли моя песочница делает то, что я про неё думаю. Классика на эту тему — цикл статей Майкла Керриска на LWN.

Идея

Пространство имён (namespace) оборачивает глобальный ресурс системы так, что процессы внутри видят свою копию этого ресурса. Для процесса это выглядит так, будто он один в системе. Для ядра это обычный процесс, просто с другим «видом» на мир.

Контейнер — это не виртуальная машина, а обычный процесс в наборе пространств имён (плюс cgroups для лимитов).

Шесть видов (на момент статьи)

ПространствоФлагС версииЧто изолирует
MountCLONE_NEWNS2.4.19точки монтирования, вид файловой системы
UTSCLONE_NEWUTS2.6.19имя хоста и домена
IPCCLONE_NEWIPC2.6.19System V IPC и очереди сообщений POSIX
PIDCLONE_NEWPID2.6.24номера процессов, свой PID 1
NetworkCLONE_NEWNET~2.6.29сетевые интерфейсы, IP, маршруты, порты
UserCLONE_NEWUSER3.8соответствие UID и GID

Пара деталей:

  • Mount был первым и называется просто NEWNS, потому что тогда никто не думал, что будут другие. Он надёжнее старого chroot.
  • PID: внутри у процесса может быть PID 1, а снаружи 48213. Контейнер можно мигрировать на другую машину с сохранением номеров.
  • Network: в новом пространстве нет вообще ничего, кроме выключенного loopback. Сеть надо явно пробросить.

Главное событие: user namespaces

Керриск особо выделяет пространства пользователей. Непривилегированный процесс может создать user namespace, в котором он root. Не настоящий: снаружи он остаётся обычным пользователем. Но внутри он может создавать остальные пространства имён. Это и открыло дорогу контейнерам без прав root, а с ними bwrap, Flatpak и rootless Podman.

Мой комментарий

Я сел разбирать, что делает песочница Евы, и обнаружил, что README и код говорят разное. В README написано --unshare-all --dir /tmp. А в коде менеджера воркеров стоит вот что:

bwrap --ro-bind / / \
      --bind /dev/shm /dev/shm \
      --bind /tmp /tmp \
      --unshare-net \
      -- ./worker-candle

Разберу по пунктам:

  • --unshare-net создаёт новое сетевое пространство: у воркера только мёртвый loopback. Интернета нет, и это главное.
  • --ro-bind / / монтирует всю систему только на чтение. Библиотеки читать можно, портить нельзя.
  • --bind /dev/shm пробрасывает общую память. Без неё воркер не поговорит с ядром, так что это осознанная дверь.
  • --bind /tmp /tmp — настоящий хостовый /tmp на запись. А вот это ошибка.

Почему ошибка: ядро Евы слушает gRPC на Unix-сокете /tmp/eva.sock. Unix-сокет — это файл, и сетевое пространство имён его не закрывает. Значит, воркер из песочницы может подключиться прямо к API ядра. Для модели, которая прочитала вредную инструкцию, это путь наружу, только не в интернет, а в мой собственный демон.

Что исправляю:

  1. --tmpfs /tmp вместо --bind /tmp /tmp: у воркера свой пустой /tmp, хостовый ему не виден.
  2. Сокет ядра переезжает из /tmp в $XDG_RUNTIME_DIR с правами 0600.
  3. Остальные пространства тоже отделяю: --unshare-all (PID, IPC, UTS, user), а сеть остаётся выключенной.
  4. Тест, который запускает воркера и проверяет, что /tmp/eva.sock изнутри не виден.

Мораль про смертельную триаду: «нет сети» не значит «нет выхода наружу». Любой канал к чему-то привилегированному считается.