Вам прислали бинарник «просто запусти и проверь». Или CI гоняет npm install с сотнями транзитивных зависимостей, каждая из которых выполняет свой postinstall. Или вы собираете чужой проект из исходников. Во всех случаях вопрос один: как выполнить недоверенный код, не отдав ему свой домашний каталог, SSH-ключи и сеть?
Docker для этого часто избыточен: демон, образы, слои — ради того, чтобы один раз запустить одну команду. Bubblewrap (bwrap) решает ровно эту задачу: непривилегированная песочница для одной команды, без демона и образов. Это тот же движок, на котором построена изоляция Flatpak.
В статье
- Что такое Bubblewrap и чем он не является
- Что он изолирует, а что нет
- Практика: изолируем недоверенную команду
- bwrap против Docker, Podman и Firejail
- Подводные камни
- Итог: когда брать bwrap
Что такое Bubblewrap и чем он не является
Bubblewrap — маленькая утилита, которая строит изолированное окружение из Linux namespaces и запускает в нём указанную команду. Никакого фонового процесса: bwrap … команда разворачивает песочницу, выполняет команду и исчезает вместе с ней.
Ключевое — непривилегированность. Через user namespaces bwrap создаёт окружение, где ваш обычный пользователь выглядит как root внутри песочницы, оставаясь непривилегированным снаружи. Root на хосте не нужен (в дистрибутивах со включёнными unprivileged user namespaces; про исключения — ниже).
Чем bwrap не является:
- Не менеджер контейнеров: нет образов, реестров, слоёв и
Dockerfile. Файловая система песочницы собирается из bind-маунтов хостовых каталогов. - Не оркестратор и не средство доставки сервисов: он не про «упаковать и выкатить», а про «запустить здесь и сейчас в изоляции».
- Не полноценная виртуализация: ядро общее с хостом. Это граница модели угроз, а не деталь реализации.
Что он изолирует, а что нет
Bubblewrap оперирует теми же примитивами, что и контейнеры, — namespaces ядра:
- mount — своя файловая система: хост виден только через явные bind-маунты.
- pid — свои процессы: код в песочнице не видит и не может сигналить процессам хоста.
- net — сетевой namespace. Важная тонкость: по умолчанию песочница разделяет сеть хоста (полный доступ, включая loopback-сервисы хоста). Изоляция сети включается явным
--unshare-net(входит в--unshare-all) — сама по себе она не появляется. - user — своё отображение UID/GID: то, что делает запуск непривилегированным.
- ipc, uts, cgroup — изоляция System V IPC, hostname и cgroup-иерархии.
Что при этом не обеспечивается автоматически:
- Защита от уязвимостей ядра. Ядро общее; эксплойт локального повышения привилегий в ядре обходит песочницу. Bubblewrap снижает поверхность атаки, но не заменяет VM для по-настоящему враждебного кода.
- seccomp по умолчанию. Сам
bwrapне ставит seccomp-фильтр — он лишь принимает ваш через--seccomp. Flatpak поставляет свой профиль; если запускаете недоверенный код, фильтр системных вызовов имеет смысл добавить самостоятельно. - Изоляция GUI. X11 не изолирует клиентов друг от друга: приложение в песочнице с доступом к сокету X может снимать нажатия клавиш и содержимое других окон. Для GUI нужен Wayland или вложенный X-сервер.
Практика: изолируем недоверенную команду
Начнём с максимально закрытой песочницы: root-файловая система только для чтения, отдельные /tmp и /proc, все namespaces отделены, сети нет.
bwrap \
--ro-bind /usr /usr \
--ro-bind /etc /etc \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/sbin /sbin \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
--unshare-all \
--die-with-parent \
--new-session \
--clearenv \
--setenv PATH /usr/bin \
/bin/bashРазбор ключевых флагов:
--ro-bind /usr /usr— система доступна только для чтения.--symlink usr/bin /binучитывает merged-/usr(в большинстве современных дистрибутивов/bin,/lib— это симлинки в/usr). Если у вас не merged-/usr, замените симлинки на--ro-bind /bin /binи т.д.--proc /procмонтирует свежийprocfs(для этого нужен pid namespace, его даёт--unshare-all),--dev /dev— минимальныйdevtmpfs.--tmpfs /tmp— эфемерный/tmp, ничего не утекает на диск хоста.--unshare-allотделяет все доступные namespaces, включая сеть, — команда полностью офлайн. Строго говоря, это «все возможные» namespaces вместе с--unshare-user-try: если user namespaces запрещены политикой системы,bwrapможет вообще не стартовать. Домашнего каталога в песочнице нет — при обращении к$HOMEкоманда упрётся в отсутствующий путь.--die-with-parentубивает песочницу, если родитель умер;--new-sessionзащищает от инъекций в терминал черезTIOCSTI;--clearenvобнуляет окружение (иначе токены и секреты из ваших env-переменных протекут внутрь).
Теперь практичный случай: дать недоверенному коду только рабочий каталог на запись, всё остальное — read-only.
bwrap \
--ro-bind /usr /usr \
--ro-bind /etc /etc \
--symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--symlink usr/bin /bin --symlink usr/sbin /sbin \
--proc /proc --dev /dev --tmpfs /tmp \
--bind "$PWD/untrusted" /work \
--chdir /work \
--unshare-all --die-with-parent --new-session \
/bin/bash ./run.shСкрипт видит и меняет только /work, но не ваш $HOME, не остальную файловую систему и не сеть.
А что с npm install, которому сеть нужна? Здесь компромисс осознанный: файловую систему запираем, а сеть оставляем. Вместо --unshare-all перечисляем namespaces вручную и не трогаем сетевой; чтобы резолвился DNS, пробрасываем resolv.conf.
bwrap \
--ro-bind /usr /usr \
--ro-bind /etc/ssl /etc/ssl \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \
--ro-bind /etc/hosts /etc/hosts \
--symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--symlink usr/bin /bin --symlink usr/sbin /sbin \
--proc /proc --dev /dev --tmpfs /tmp \
--bind "$PWD" /app --chdir /app \
--clearenv \
--setenv PATH /usr/bin \
--setenv HOME /app \
--unshare-user --unshare-pid --unshare-ipc --unshare-uts \
--die-with-parent --new-session \
npm installСеть доступна (мы не сделали --unshare-net), но --clearenv вычищает окружение — иначе NPM_TOKEN, AWS_ACCESS_KEY_ID и прочие секреты из вашей сессии протекли бы внутрь к недоверенному коду. А любой postinstall-скрипт заперт в /app и не дотянется до ~/.ssh или ~/.aws. --setenv HOME /app уводит кеш npm в песочницу, где домашнего каталога иначе нет. Это типовой паттерн: изолировать то, что важно (файлы, процессы, секреты в env), и сознательно оставить то, без чего задача не работает (сеть). Помните о цене компромисса: сеть оставлена полностью — недоверенный postinstall не доберётся до $HOME, но сможет ходить в интернет и стучаться в loopback-сервисы хоста (localhost-демоны, облачные метаданные 169.254.169.254). Если это неприемлемо — добавьте --unshare-net или сетевой фильтр.
bwrap против Docker, Podman и Firejail
| Bubblewrap | Docker | Podman | Firejail | |
|---|---|---|---|---|
| Механизм | namespaces (+ ваш seccomp) | namespaces + cgroups + образы | то же, без демона | namespaces + seccomp + профили |
| Демон | нет | dockerd |
нет | нет |
| Привилегии | непривилегированный (userns) | демон под root (или rootless) | заточен под rootless | традиционно setuid root |
| Файловая система | ФС хоста через bind | слоёные образы | слоёные образы | ФС хоста + профили |
| Сеть | как у хоста; отключается --unshare-net |
bridge/NAT | bridge/NAT | фильтры |
| Состояние | эфемерное, без образов | volumes/образы | volumes/образы | эфемерное |
| Ниша | изолировать одну команду | упаковать и доставить сервис | rootless-контейнеры | песочница для desktop-приложений |
Когда bwrap уместнее контейнеров:
- Нужно изолировать одну команду, сборку или скрипт здесь и сейчас — без Dockerfile, образа и демона.
- Хочется непривилегированности без настройки rootless-стека.
- Вы строите изоляцию как примитив внутри своего инструмента (именно так его использует Flatpak).
Когда bwrap не замена:
- Доставка и версионирование сервисов — это про образы и оркестрацию (Docker/Podman/Kubernetes).
- Firejail удобнее для десктопных приложений: готовые профили под браузеры и мессенджеры.
bwrap— более низкоуровневый конструктор без базы профилей. - Podman ближе, если нужен полноценный OCI-контейнер, но rootless и без демона.
Подводные камни
- Unprivileged user namespaces могут быть выключены. На части дистрибутивов и hardened-ядер непривилегированные user namespaces ограничены или отключены (
kernel.unprivileged_userns_clone=0, политики AppArmor/Debian). Современные версииbwrapв этом случае просто не стартуют — им нужны доступные user namespaces. Исторически существовалsetuid-режим как обходной путь (его ещё можно встретить в старых и некоторых дистрибутивных сборках), но в актуальном upstream он удалён — закладываться на него не стоит. - Общее ядро — общая судьба. Против уязвимости ядра песочница не спасёт. Для враждебного кода с высокой ценой компрометации нужна виртуализация (VM, gVisor, Kata).
- Без seccomp вы не фильтруете системные вызовы. Для недоверенного кода добавьте
--seccompс продуманным профилем; голая изоляция namespaces оставляет всю поверхность syscall открытой. - GUI и звук — отдельная история. Проброс сокета X11 фактически снимает изоляцию ввода. Wayland, вложенный X или PipeWire с портфелем разрешений — обязательны, если пускаете GUI-приложение.
Итог: когда брать bwrap
Bubblewrap — это не «маленький Docker», а другой инструмент под другую задачу: быстро и без привилегий изолировать выполнение одной команды. Он незаменим, когда нужно запустить чужой бинарник, прогнать сборку или ограничить postinstall-скрипты, не поднимая контейнерный стек. Его же берут как строительный блок изоляции внутри собственных утилит.
Помните про границы: общее ядро, seccomp — на вашей ответственности, а user namespaces могут быть недоступны. Для недоверенного кода комбинируйте bwrap с seccomp-фильтром и bind-мауньте минимально необходимое — тогда «просто запусти и проверь» перестаёт быть русской рулеткой.
Комментарии