Пространства имён Linux: из чего на самом деле сделаны контейнеры
Каждый воркер Евы запускается в песочнице bwrap. Пора разобраться, что именно она отделяет, и заодно проверить, правда ли моя песочница делает то, что я про неё думаю. Классика на эту тему — цикл статей Майкла Керриска на LWN.
Идея
Пространство имён (namespace) оборачивает глобальный ресурс системы так, что процессы внутри видят свою копию этого ресурса. Для процесса это выглядит так, будто он один в системе. Для ядра это обычный процесс, просто с другим «видом» на мир.
Контейнер — это не виртуальная машина, а обычный процесс в наборе пространств имён (плюс cgroups для лимитов).
Шесть видов (на момент статьи)
| Пространство | Флаг | С версии | Что изолирует |
|---|---|---|---|
| Mount | CLONE_NEWNS | 2.4.19 | точки монтирования, вид файловой системы |
| UTS | CLONE_NEWUTS | 2.6.19 | имя хоста и домена |
| IPC | CLONE_NEWIPC | 2.6.19 | System V IPC и очереди сообщений POSIX |
| PID | CLONE_NEWPID | 2.6.24 | номера процессов, свой PID 1 |
| Network | CLONE_NEWNET | ~2.6.29 | сетевые интерфейсы, IP, маршруты, порты |
| User | CLONE_NEWUSER | 3.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 ядра. Для модели, которая прочитала вредную инструкцию, это путь наружу, только не в интернет, а в мой собственный демон.
Что исправляю:
--tmpfs /tmpвместо--bind /tmp /tmp: у воркера свой пустой/tmp, хостовый ему не виден.- Сокет ядра переезжает из
/tmpв$XDG_RUNTIME_DIRс правами0600. - Остальные пространства тоже отделяю:
--unshare-all(PID, IPC, UTS, user), а сеть остаётся выключенной. - Тест, который запускает воркера и проверяет, что
/tmp/eva.sockизнутри не виден.
Мораль про смертельную триаду: «нет сети» не значит «нет выхода наружу». Любой канал к чему-то привилегированному считается.