Bubblewrap/示例
创建一个简单的 dhcpcd 沙箱:
- 确定可用的内核命名空间(kernel namespaces):
$ ls /proc/self/ns cgroup ipc mnt net pid uts
user 意味着内核以 CONFIG_USER_NS=n 参数构建或是被限制了用户命名空间(user namespace)。- 将宿主的整个
/目录以读写模式绑定到沙箱中的/。 - 将一个新的 devtmpfs 文件系统挂载到沙箱中的
/dev。 - 创建一个新的 IPC(进程间通信) 和 cgroups(控制组) 命名空间。
- 创建一个新的 UTS 命名空间并将
dhcpcd设置为主机名(hostname)。
# /usr/bin/bwrap --bind / / --dev /dev --unshare-ipc --unshare-cgroup --unshare-uts --hostname dhcpcd /usr/bin/dhcpcd -q -b
创建一个稍颗粒化和复杂的 Unbound 沙箱:
- 将宿主系统的
/usr目录以只读模式绑定到沙箱中的/usr。 - 将宿主系统的
/usr/lib目录软链接到沙箱中的/lib64。 - 将宿主系统的
/etc目录以只读模式绑定到沙箱中的/etc。 - 在沙箱中创建空的
/var和/run目录。 - 将一个新的 devtmpfs 文件系统挂载到沙箱中的
/dev。 - 创建一个新的 IPC(进程间通信)和 PID(进程标识符) 以及控制组命名空间(control group namespaces)。
- 创建一个新的 UTS 命名空间并将
unbound设置为主机名(hostname)。
# /usr/bin/bwrap --ro-bind /usr /usr --symlink usr/lib /lib64 --ro-bind /etc /etc --dir /var --dir /run --dev /dev --unshare-ipc --unshare-pid --unshare-cgroup --unshare-uts --hostname unbound /usr/bin/unbound -d
unbound.service。
在 shell 包装器(wrapper)中创建环境时,能体现出 bwrap 的强大和灵活性:
- 将宿主系统的
/usr/bin目录以只读模式绑定到沙箱中的/usr/bin。 - 将宿主系统的
/usr/lib目录以只读模式绑定到沙箱中的/usr/lib。 - 将宿主系统的
/usr/lib目录软链接到沙箱中的/lib64。 - 在沙箱中创建一个 tmpfs 文件系统,覆盖
/usr/lib/gcc。- 这么做可以有效地阻止
/usr/lib/gcc中的内容出现在沙箱中。
- 这么做可以有效地阻止
- 在沙箱中创建一个新的 tmpfs 文件系统,作为
$HOME目录。 - 将
.Xauthority文件和“Documents”目录以只读模式绑定到沙箱。- 这么做可以有效且递归地允许访问
.Xauthority文件和“Documents”目录。
- 这么做可以有效且递归地允许访问
- 在沙箱中创建一个新的 tmpfs 文件系统,作为
/tmp目录。 - 将 X11 socket 以只读模式绑定到沙箱中以允许访问。
- 复制并创建用于容纳宿主内核所支持的所有命名空间的私有容器(private containers)。
- 如果内核不支持非特权的(non-privileged)用户命名空间,跳过创建过程并继续操作。
- 勿将网络组件放置到私有命名空间内。
- 这么做可以允许联网访问 URI 超链接。
#!/bin/sh #~/bwrap/mupdf.sh (exec bwrap \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib /usr/lib \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/gcc \ --tmpfs $HOME \ --ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --ro-bind $HOME/Documents $HOME/Documents \ --tmpfs /tmp \ --ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \ --unshare-all \ --share-net \ /usr/bin/mupdf "$@")
/usr/bin/sh 执行作为已存在的可执行项目的替代的命令包装器以调试和验证沙箱中的内容和文件系统结构。$ bwrap \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib /usr/lib \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/gcc \ --tmpfs $HOME \ --ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --ro-bind $HOME/Desktop $HOME/Desktop \ --tmpfs /tmp \ --ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \ --unshare-all \ --share-net \ /usr/bin/sh bash-4.4$ ls -AF .Xauthority Documents/
或许在构建一个被 bubblewrap 的文件系统时需考虑到的最重要的规则是:“命令应该按出现的顺序依次执行”。 根据上面的 MuPDF 示例:
- 首先创建了一个 tmpfs 文件系统,其次是绑定并挂载
.Xauthority文件和“Documents”目录:
--tmpfs $HOME \ --ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --ro-bind $HOME/Documents $HOME/Documents \
bash-4.4$ ls -a . .. .Xauthority Desktop
- 如果在绑定挂载
.Xauthority后再创建 tmpfs 文件系统,结果将覆盖先前文件,因此沙箱中只能够找到“Documents”这个目录:
--ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --tmpfs $HOME \ --ro-bind $HOME/Desktop $HOME/Desktop \
bash-4.4$ ls -a . .. Desktop
还未对已知漏洞 进行修补的应用程序是 bubblewrap 的主要候选对象:
- 将宿主系统中的
/usr/bin/7za可执行文件路径以只读模式绑定到沙箱中。 - 将宿主系统的
/usr/lib目录软链接到沙箱中的/lib64。 - 用 tmpfs 文件系统覆盖以禁用沙箱中
/usr/lib/modules和/usr/lib/systemd的内容。 - 将一个新的 devtmpfs 文件系统挂载到沙箱中的
/dev。 - 将宿主系统的
/sandbox目录以读写模式绑定到沙箱中的/sandbox。-
7za 仅会在宿主的
/sandbox目录中运行,或在被 shell 包装器调用时,在其子文件夹中运行。
-
7za 仅会在宿主的
- 为应用程序及其进程创建新的 cgroup、IPC、network、PID 和 UTS 命名空间。
- 如果内核不支持非特权的用户命名空间,跳过创建过程并继续操作。
- 新创建的 network 命名空间将阻止沙箱获取网络访问权限。
- 为沙箱设置计算机名,例如
p7zip。 - 取消设置
XAUTHORITY环境变量以隐藏 X11 conection cookie 的位置。- 7za 正常运行时不需要连接到 X11 显示服务。
- 启动一个新的终端会话,防止键盘输入从沙箱中逃逸。
#!/bin/sh #~/bwrap/pz7ip.sh (exec bwrap \ --ro-bind /usr/bin/7za /usr/bin/7za \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/modules \ --tmpfs /usr/lib/systemd \ --dev /dev \ --bind /sandbox /sandbox \ --unshare-all \ --hostname p7zip \ --unsetenv XAUTHORITY \ --new-session \ /usr/bin/7za "$@")
bwrap \ --ro-bind /usr/bin/7za /usr/bin/7za \ --ro-bind /usr/bin/ls /usr/bin/ls \ --ro-bind /usr/bin/sh /usr/bin/sh \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/modules \ --tmpfs /usr/lib/systemd \ --dev /dev \ --bind /sandbox /sandbox \ --unshare-all \ --hostname p7zip \ --unsetenv XAUTHORITY \ --new-session \ /usr/bin/sh bash: no job control in this shell bash-4.4$ ls -AF dev/ lib64@ usr/ bash-4.4$ ls -l /usr/lib/modules total 0 bash-4.4$ ls -l /usr/lib/systemd total 0 bash-4.4$ ls -AF /dev console full null ptmx@ pts/ random shm/ stderr@ stdin@ stdout@ tty urandom zero bash-4.4$ ls -A /usr/bin 7za ls sh
面向网络且拥有巨大的潜在被攻击面的应用程序同样是 bubblewrap 的理想候选对象:
- 将 Transmission 包含在沙箱中以响应磁力链接和种子链接并启动。
- 示例的 wrap 在GNOME(Wayland)下支持音频(PulseAudio)和打印服务(CUPS/Avahi)。
-
~/.config/transmission/settings.json中的 PATH 应反映--setenv HOME变量。
-
- 环境中的按键绑定不支持变量扩展,因此使用了完整路径。
- 同时还支持 WebRenderer 和硬件加速混成。
bwrap \ --symlink usr/lib /lib \ --symlink usr/lib64 /lib64 \ --symlink usr/bin /bin \ --symlink usr/bin /sbin \ --ro-bind /usr/lib /usr/lib \ --ro-bind /usr/lib64 /usr/lib64 \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib/firefox /usr/lib/firefox \ --ro-bind /usr/share/applications /usr/share/applications \ --ro-bind /usr/share/gtk-3.0 /usr/share/gtk-3.0 \ --ro-bind /usr/share/fontconfig /usr/share/fontconfig \ --ro-bind /usr/share/icu /usr/share/icu \ --ro-bind /usr/share/drirc.d /usr/share/drirc.d \ --ro-bind /usr/share/fonts /usr/share/fonts \ --ro-bind /usr/share/glib-2.0 /usr/share/glib-2.0 \ --ro-bind /usr/share/glvnd /usr/share/glvnd \ --ro-bind /usr/share/icons /usr/share/icons \ --ro-bind /usr/share/libdrm /usr/share/libdrm \ --ro-bind /usr/share/mime /usr/share/mime \ --ro-bind /usr/share/X11/xkb /usr/share/X11/xkb \ --ro-bind /usr/share/icons /usr/share/icons \ --ro-bind /usr/share/mime /usr/share/mime \ --ro-bind /etc/fonts /etc/fonts \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --ro-bind /usr/share/ca-certificates /usr/share/ca-certificates \ --ro-bind /etc/ssl /etc/ssl \ --ro-bind /etc/ca-certificates /etc/ca-certificates \ --dir "$XDG_RUNTIME_DIR" \ --ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \ --ro-bind "$XDG_RUNTIME_DIR/wayland-1" "$XDG_RUNTIME_DIR/wayland-1" \ --dev /dev \ --dev-bind /dev/dri /dev/dri \ --ro-bind /sys/dev/char /sys/dev/char \ --ro-bind /sys/devices/pci0000:00 /sys/devices/pci0000:00 \ --proc /proc \ --tmpfs /tmp \ --bind /home/example/.mozilla /home/example/.mozilla \ --bind /home/example/.config/transmission /home/example/.config/transmission \ --bind /home/example/Downloads /home/example/Downloads \ --setenv HOME /home/example \ --setenv GTK_THEME Adwaita:dark \ --setenv MOZ_ENABLE_WAYLAND 1 \ --setenv PATH /usr/bin \ --hostname RESTRICTED \ --unshare-all \ --share-net \ --die-with-parent \ --new-session \ /usr/bin/firefox
- 移除特定条目可以增加访问限制。
- 移除下面的条目可以取消音频支持:
--ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \
-
/sandbox作为用户所定义的目录,并没有特殊含义,仅用于存放所需的个人资料信息。- 可通过脚本或定时任务,也可手动将清理过的配置文件复制到
/sandbox。
- 可通过脚本或定时任务,也可手动将清理过的配置文件复制到
$ cp -pR ~/.mozilla /sandbox/
文件位置可以来自网络共享、USB 挂载设备,或是来自本地文件系统,甚至是 ramfs 或 tmpfs。
- 设置
/home/r以隐藏真实的/home/example。 - 设置新的用户标识符和组标识符。
/etc/passwd 和 /etc/groups 中已存在的值相冲突。bwrap \ .... --bind /sandbox/.mozilla /home/r/.mozilla \ --bind /sandbox/Downloads /home/r/Downloads \ ... --setenv HOME /home/r \ ... --uid 200 --gid 400 \ ... /usr/bin/firefox --no-remote --private-window
下面是 Wayland 下运行 Chromium 沙箱的简单示例:
bwrap \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/bin /sbin \
--ro-bind /usr/lib /usr/lib \
--ro-bind /usr/lib64 /usr/lib64 \
--ro-bind /usr/bin /usr/bin \
--ro-bind /etc /etc \
--ro-bind /usr/lib/chromium /usr/lib/chromium \
--ro-bind /usr/share /usr/share \
--dev /dev \
--dev-bind /dev/dri /dev/dri \
--proc /proc \
--ro-bind /sys/dev/char /sys/dev/char \
--ro-bind /sys/devices /sys/devices \
--ro-bind /run/dbus /run/dbus \
--dir "$XDG_RUNTIME_DIR" \
--ro-bind "$XDG_RUNTIME_DIR/wayland-1" "$XDG_RUNTIME_DIR/wayland-1" \
--ro-bind "$XDG_RUNTIME_DIR/pipewire-0" "$XDG_RUNTIME_DIR/pipewire-0" \
--ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \
--tmpfs /tmp \
--dir $HOME/.cache \
--bind $HOME/.config/chromium $HOME/.config/chromium \
--bind $HOME/Downloads $HOME/Downloads \
/usr/bin/chromium --enable-features=UseOzonePlatform --ozone-platform=wayland
kernel.unprivileged_userns_clone sysctl 已被设置为 0,会无法实现沙箱运行 chromium。用户可以自行设置为 1,但不建议这么做(FS#36969)。
一种解决方法是让 chromium 使用 bubblewrap 创建的命名空间。可以用 zypakAUR 实现,这个方法同样被 flatpak 用于在一个额外命名空间中运行基于 electron 的应用程序。这个链接是有关如何使用 zypak 运行 chromium/electron 的示例代码。
-
PipeWire:
--ro-bind "$XDG_RUNTIME_DIR/pipewire-0" "$XDG_RUNTIME_DIR/pipewire-0" \- 不使用 pipewire 的用户可移除此行内容。
-
--bind $HOME/.config/chromium $HOME/.config/chromium \将用户的 chromium 配置目录以读写模式挂载到沙箱中。 -
--bind $HOME/Downloads $HOME/Downloads \将用户的“~/Downloads”目录以读写模式挂载到沙箱中。 - 通过增加更多隔离处理可优化本例。
Steam 正常运行时需要访问 D-Bus。
下面的示例使用 xdg-dbus-proxy 将有限的宿主系统 bus 访问权限暴露给沙箱。一旦 bwrap 退出,此 proxy 将被一同终止。
steam.py
#!/usr/bin/env python3
from glob import glob
import os
from pathlib import Path
import subprocess
HOME = Path.home()
XDG_RUNTIME_DIR = Path(os.getenv("XDG_RUNTIME_DIR", "/tmp"))
XDG_CACHE_HOME = Path(os.getenv("XDG_CACHE_HOME", HOME / ".cache"))
XDG_DATA_HOME = Path(os.getenv("XDG_DATA_HOME", HOME / ".local/share"))
DBUS_PROXY_PATH = XDG_RUNTIME_DIR / "bus-proxy"
DBUS_PROXY_PATH.mkdir(exist_ok=True, parents=True)
#
STEAM_HOME = XDG_DATA_HOME / "steam_home"
STEAM_HOME.mkdir(exist_ok=True, parents=True)
# xdg-dbus-proxy
r, w = os.pipe()
session_bus_proxy = DBUS_PROXY_PATH / str(os.getpid())
system_bus_proxy = DBUS_PROXY_PATH / f"{os.getpid()}-system"
subprocess.Popen([
"/usr/bin/xdg-dbus-proxy",
f"--fd={w}",
# session bus
os.environ["DBUS_SESSION_BUS_ADDRESS"],
str(session_bus_proxy),
"--filter",
"--own=com.steampowered.*",
"--talk=org.freedesktop.portal.*",
"--talk=org.gnome.SettingsDaemon.MediaKeys",
"--talk=org.kde.StatusNotifierWatcher",
"--talk=org.freedesktop.ScreenSaver",
"--talk=org.freedesktop.PowerManagement",
"--talk=org.freedesktop.Notifications",
# systrem bus
"unix:path=/run/dbus/system_bus_socket",
str(system_bus_proxy),
"--filter",
"--talk=org.freedesktop.UPower",
"--talk=org.freedesktop.UDisks2", # used by wine
], pass_fds=[w])
# wait xdg-dbus-proxy to start
os.read(r, 1)
os.set_inheritable(r, True)
# bwrap
argv: list[str] = [
"bwrap",
"--bind", str(STEAM_HOME), str(HOME),
"--proc", "/proc",
"--dev", "/dev",
"--dir", "/tmp",
"--unshare-cgroup-try",
"--unshare-pid",
"--unshare-user-try",
"--unshare-uts",
"--die-with-parent",
"--sync-fd", str(r), # ensures dbus proxy stops when the bwrap bwrap quits
"--bind", str(system_bus_proxy), "/run/dbus/system_bus_socket",
"--bind", str(session_bus_proxy), str(XDG_RUNTIME_DIR / "bus"),
]
def rw(*paths): argv.extend([i for path in paths for i in ("--bind-try", str(path), str(path))])
def ro(*paths): argv.extend([i for path in paths for i in ("--ro-bind-try", str(path), str(path))])
def dev(*paths): argv.extend([i for path in paths for i in ("--dev-bind-try", str(path), str(path))])
def link(*links): argv.extend([i for link in links for i in ("--symlink", str(link[0]), str(link[1]))])
rw(
"/usr",
"/etc",
"/opt",
"/run/systemd/resolve/",
# WARNING: Forwarding the X11 is insecure and might lead to sandbox escapes
"/tmp/.X11-unix",
"/tmp/.ICE-unix",
*glob(str(XDG_RUNTIME_DIR / "wayland*")),
# audio
"/var/lib/alsa/",
*glob(str(XDG_RUNTIME_DIR / "pulse*")),
*glob(str(XDG_RUNTIME_DIR / "pipewire*")),
# shader cache
XDG_CACHE_HOME / "mesa_shader_cache",
XDG_CACHE_HOME / "mesa_shader_cache_db",
XDG_CACHE_HOME / "nv",
XDG_CACHE_HOME / "nvidia",
XDG_CACHE_HOME / "radv_builtin_shaders",
XDG_CACHE_HOME / "radv_builtin_shaders64",
# steam
XDG_DATA_HOME / "Steam",
)
link(
("/usr/bin", "/bin"),
("/usr/bin", "/sbin"),
("/usr/lib", "/lib"),
("/usr/lib64", "/lib64"),
("/run", "/var/run"),
)
dev(
"/dev/dri",
"/dev/input",
"/dev/hugepages",
*glob("/dev/nvidia*"),
"/dev/snd",
"/dev/fuse",
"/sys/block/",
"/sys/bus/",
"/sys/class/",
"/sys/dev/",
"/sys/devices/",
"/sys/module/",
)
os.execvp("bwrap", argv + ["--sync-fd", str(r), "/usr/lib/steam/steam"])
- 本例中将
$XDG_DATA_HOME/steam_home挂载为$HOME。用户可以自行将STEAM_HOME修改为其他位置。 - Use the
rw()orro()functions to grant read/write or read-only access, respectively, to a host path. For example,rw("HOME/.config/unity3d"). - 使用
rw()或ro()以授予宿主 path 的读写或只读权限。例如rw("HOME/.config/unity3d")。
为能在项目的工作目录中使用 bubblewrap 运行“npm”,可以参考下面的命令示例。
Bubblewrap 能够与 Angular、Cypress 和 Maven Java 共同使用并工作良好。X11 和 Wayland 需要被包含在最开始,因为 Cypress 会启动一个基于 Electron 的图形界面。
假设用户在项目工作目录下执行 npm install,同时“npm”需要读写 node_modules、package.json 等文件,这种方式允许在程序启动目录中拥有完整的文件访问权限。同时“npm”和“nvm”(npm -g install ...)也可以访问的全局安装目录。此外 Cypress 也可以运行在 X11 或 Wayland 下。
bwrap_arguments=(
# 避免成为僵尸进程
--die-with-parent
# 依赖项需要网络访问
--unshare-all
--share-net
# 创建正确的运行环境
--tmpfs /
--tmpfs /run
--dir /tmp
--dev /dev
--proc /proc
--ro-bind /bin /bin
--ro-bind /sbin /sbin
--ro-bind /usr /usr
--ro-bind /etc /etc
--ro-bind /lib /lib
--ro-bind /lib64 /lib64
--ro-bind /sys /sys
--ro-bind /var /var
# systemd-resolve for dns
--ro-bind /run/systemd/resolve /run/systemd/resolve
# npm 初始化仓库时会使用 git,确保已配置好 email 和 username 项
--ro-bind $XDG_CONFIG_HOME/git/config $XDG_CONFIG_HOME/git/config
# zsh has to look everywhere cool
--ro-bind $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshrc
--ro-bind $XDG_CONFIG_HOME/zsh/.zshenv $XDG_CONFIG_HOME/zsh/.zshenv
--ro-bind $HOME/.zshenv $HOME/.zshenv
# Maven
--ro-bind /opt/maven /opt/maven
--ro-bind $HOME/.m2 $HOME/.m2
# NPM
--bind "$XDG_DATA_HOME/npm" "$XDG_DATA_HOME/npm"
# npm、cypress、nvm、maven 等程序需要使用 cache
--bind "$XDG_CACHE_HOME" "$XDG_CACHE_HOME"
# x11, needed for cypress
--ro-bind "$XAUTHORITY" "$XAUTHORITY"
# wayland, might be useful
--ro-bind "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
# 假定运行时所在的目录是项目工作目录,并且拥有完全的访问权限
--bind "$(pwd)" "$(pwd)"
)
# 以上方指定的参数运行 bwrap 处理的用户命令:
$ bwrap "${bwrap_arguments[@]}" "$@"