使用 Bubblewrap 在 Linux 上轻松实现沙箱

Lobsters Hottest 工具

摘要

一篇博客文章,介绍了一种在 Linux 上使用 Bubblewrap 的轻量级沙箱方法,其中名为 'box' 的脚本以只读方式共享主机文件系统,并以可读写方式共享当前目录,从而无需单独发行版即可在隔离环境中运行主机二进制文件。

<p><a href="https://lobste.rs/s/stehhb/easy_sandboxing_on_linux_with_bubblewrap">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/10 10:57

# 在 Linux 上使用 Bubblewrap 轻松进行沙盒隔离 来源:https://bxt.rs/blog/easy-sandboxing-on-linux-with-bubblewrap/ 2026年8月9日 19:51·1531 字·8 分钟阅读 在这个动荡的时代,人们经常需要在沙盒中运行某些工具。主要目的是减小爆炸半径:让沙盒内的程序无法破坏主机系统(例如删除或覆盖非预期内容),同时在较小程度上隐藏大部分文件系统,以避免敏感数据被窃取。 最近,[Bartosz Taudul](https://github.com/wolfpld/tracy)(因 [Tracy](https://github.com/wolfpld/tracy) 而闻名)[展示](https://wolf.nereid.pl/posts/systemd-nspawn/)了如何为此目的使用 [systemd-nspawn](https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html)。他创建了一个容器配置,在其中安装了一个发行版,并绑定挂载了主机上的一些缓存和项目文件夹。这些挂载之上还有 overlayfs,因此容器内的工具可以覆盖这些文件,但这些写入不会影响主机文件系统。 我也想分享我的沙盒方案。我的目标是让它易于使用,并尽可能减少使用障碍,这样我随时都能有一个沙盒可用。 最终方案归结为:启动一个类容器环境,以只读方式共享足够多的主机文件系统,使所有主机二进制文件都能运行,并以读写方式共享当前工作目录。在这个沙盒中,你不需要安装单独的发行版——主机上的一切都能正常工作,同时文件系统大多保持隔离(除了你运行沙盒所在的文件夹)。 例如,我将在 Tracy 的检出目录中运行这个脚本。 ``` ┌ ((8c8d451a)) ~/s/c/tracy └─ box fish Welcome to fish, the friendly interactive shell Type help for instructions on how to use fish yalter@sandbox ~/s/c/tracy> ``` 我可以运行构建,因为所有主机二进制文件都可访问: ``` yalter@sandbox ~/s/c/tracy> meson setup build The Meson build system Version: 1.11.2 Source dir: /home/yalter/source/cpp/tracy Build dir: /home/yalter/source/cpp/tracy/build Build type: native build Project name: tracy Project version: 0.13.1 C++ compiler for the host machine: /usr/bin/ccache c++ (clang 22.1.8 "clang version 22.1.8 (AerynOS)") C++ linker for the host machine: c++ ld.lld 22.1.8 Host machine cpu family: x86_64 Host machine cpu: x86_64 Checking if define "_MSC_VER" exists: NO Run-time dependency threads found: YES Found pkg-config: YES (/usr/bin/pkg-config) 2.5.1 Build targets in project: 1 Found ninja-1.13.2 at /usr/bin/ninja yalter@sandbox ~/s/c/tracy> ninja -C build ninja: Entering directory `build' [2/2] Linking target libtracy.so ``` home 文件夹包含工作目录,除此之外基本上是空的: ``` yalter@sandbox ~/s/c/tracy> ls -l ~ total 0 drwx------ 4 1000 1000 80 Aug 9 20:20 source/ ``` 我可以在 home 文件夹中写入,但写入会进入 tmpfs,不会影响主机系统: ``` yalter@sandbox ~/s/c/tracy> touch ~/evil yalter@sandbox ~/s/c/tracy> ^D ┌ ((8c8d451a)) ~/s/c/tracy └─ cat ~/evil cat: /home/yalter/evil: No such file or directory ``` 只有在运行沙盒的 Tracy 文件夹中的更改才会持久化到主机上,而且用户 ID 等都正确: ``` ┌ ((8c8d451a)) ~/s/c/tracy └─ ls -l build/ total 28K drwxr-xr-x 1 yalter yalter 48 Aug 9 20:21 libtracy.so.p drwxr-xr-x 1 yalter yalter 496 Aug 9 20:21 meson-info drwxr-xr-x 1 yalter yalter 56 Aug 9 20:21 meson-logs drwxr-xr-x 1 yalter yalter 310 Aug 9 20:21 meson-private drwxr-xr-x 1 yalter yalter 40 Aug 9 20:21 meson-uninstalled -rw-r--r-- 1 yalter yalter 5,3K Aug 9 20:21 build.ninja -rw-r--r-- 1 yalter yalter 545 Aug 9 20:21 compile_commands.json -rwxr-xr-x 1 yalter yalter 14K Aug 9 20:21 libtracy.so ``` ## [box 脚本](https://bxt.rs/blog/easy-sandboxing-on-linux-with-bubblewrap/#the-box-script) 我使用 [Bubblewrap](https://github.com/containers/bubblewrap) 来启动沙盒。这是一个无特权沙盒工具,被 [Flatpak](https://flatpak.org/) 使用(不过,我听说有计划用其他工具替换它)。 脚本本身组成了一个很长的 `bwrap` 调用。让我们看看其中的一些部分。 ``` #!/usr/bin/env bash set -euo pipefail # Export ALLOW_NET=0 to disable network access inside the sandbox. # # Keep in mind that if your X11/Xwayland doesn't check Xauth, # then network access lets the sandbox connect to your X11 # via an abstract Unix socket. This is quite dangerous. ALLOW_NET="${ALLOW_NET:-1}" # The current folder that we're binding read-write. REPO="$(readlink -f .)" BWRAP=( bwrap --die-with-parent # Unshare (isolate) a bunch of things inside the sandbox. --unshare-pid --unshare-uts --unshare-cgroup-try --unshare-user-try --cap-drop ALL # Create/mount important folders. --proc /proc --dev /dev --tmpfs /tmp --tmpfs /var --dir /run --dir /etc --hostname sandbox # Warning: this script shares all environment variables. # If on your system the environment can contain secrets, # you may want to clear them: # --clearenv # Bind the current folder read-write and chdir there. --bind "$REPO" "$REPO" --chdir "$REPO" ) # --- Read-only system binds --- SYS_RO_BINDS=( # Folders with binaries and libraries. /usr /bin /sbin /lib /lib64 # Random configuration files that programs tend to need. /etc/alternatives /etc/nsswitch.conf /etc/hosts /etc/localtime /etc/timezone /etc/pki /etc/ca-certificates /etc/ssl /etc/crypto-policies /etc/fonts # I fill these as I bump into problems, more or less. /etc/java /etc/texlive /var/lib/texmf /usr/lib/jvm /usr/share/java ) # Bind all of them read-only. for p in "${SYS_RO_BINDS[@]}"; do [[ -e "$p" ]] && BWRAP+=( --ro-bind "$p" "$p" ) done BWRAP+=( --ro-bind-try /etc/ld.so.cache /etc/ld.so.cache ) # resolv.conf is fun because it's a symlink into /run, # a folder which we do not want to expose. RESOLV_REAL="$(readlink -f /etc/resolv.conf 2>/dev/null || true)" if [[ -n "$RESOLV_REAL" && -f "$RESOLV_REAL" ]]; then BWRAP+=( --ro-bind "$RESOLV_REAL" /etc/resolv.conf ) fi # Unshare the network if needed. if [[ "$ALLOW_NET" -eq 0 ]]; then BWRAP+=( --unshare-net ) fi # Create a fresh home directory. # The username and the path is the same as on the host # so that everything keeps working. BWRAP+=( --setenv HOME "$HOME" --dir "$HOME" ) # --- Home read-only binds --- HOME_RO_BINDS=( .cargo/bin .cargo/config.toml .local/bin .local/lib/node_modules .rustup .fonts .local/share/fonts .local/share/nvim/site/parser .gitconfig .config/git .config/tmux .cache/ms-playwright .cache/corepack ) for rel in "${HOME_RO_BINDS[@]}"; do [[ -e "$HOME/$rel" ]] && BWRAP+=( --ro-bind "$HOME/$rel" "$HOME/$rel" ) done # --- Home overlays --- # The sandbox can write here, but the changes # will not affect the host filesystem. HOME_TMP_OVERLAYS=( .cache/fontconfig .cargo/registry .cargo/git .gradle .npm .cache/npm .local/share/pnpm/store .cache/yarn .cache/cpm .texlive2023 ) for rel in "${HOME_TMP_OVERLAYS[@]}"; do [[ -d "$HOME/$rel" ]] && BWRAP+=( --overlay-src "$HOME/$rel" --tmp-overlay "$HOME/$rel" ) done # Set up $PATH with the paths that we have inside this sandbox. BWRAP+=( --setenv PATH "$HOME/.cargo/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" ) # Execute our big commandline and pass it # the rest of the arguments (the command to run). CMD=( "${@:-bash}" ) exec "${BWRAP[@]}" "${CMD[@]}" ``` 代码有很多行,但其中大部分只是列出了要挂载的目录。 如果你想在沙盒中运行 GUI 应用,你需要创建一个 `XDG_RUNTIME_DIR` 并挂载 Wayland 套接字: ``` # Export PASS_WAYLAND=1 to enable Wayland access. # Warning: it is currently NOT SANDBOXED (e.g. with security-context protocol). # See https://niri-wm.github.io/niri/Security-Model.html#unsandboxed-clients # for an example of what that implies. PASS_WAYLAND="${PASS_WAYLAND:-0}" # Export PASS_DRI=1 to enable DRI (GPU) access for hardware acceleration. PASS_DRI="${PASS_DRI:-0}" # Export PASS_X11=1 to enable X11 (Xwayland) access. PASS_X11="${PASS_X11:-0}" if [[ "$PASS_DRI" -eq 1 && -d /dev/dri ]]; then BWRAP+=( --dev-bind /dev/dri /dev/dri ) fi # EGL complains without this. BWRAP+=( --ro-bind /sys /sys ) # Wayland: bind only the socket into a fresh runtime dir. XDG_RT="${XDG_RUNTIME_DIR:-}" WAYLAND_SOCK="${WAYLAND_DISPLAY:-wayland-0}" if [[ "$PASS_WAYLAND" -eq 1 && -n "$XDG_RT" && -S "$XDG_RT/$WAYLAND_SOCK" ]]; then BWRAP+=( --dir /run/user --dir /run/user/1000-sbox --bind "$XDG_RT/$WAYLAND_SOCK" "/run/user/1000-sbox/$WAYLAND_SOCK" --setenv XDG_RUNTIME_DIR /run/user/1000-sbox --setenv WAYLAND_DISPLAY "$WAYLAND_SOCK" ) else BWRAP+=( --unsetenv WAYLAND_DISPLAY ) fi # X11. DISPLAY_VAR="${DISPLAY:-}" if [[ "$PASS_X11" -eq 1 && -n "$DISPLAY_VAR" && -d /tmp/.X11-unix ]]; then BWRAP+=( --ro-bind /tmp/.X11-unix /tmp/.X11-unix --setenv DISPLAY "$DISPLAY_VAR" ) else # Make it harder for accidental X11: unset DISPLAY. BWRAP+=( --unsetenv DISPLAY ) fi ``` 脚本的内容差不多就这些。如果需要,只需将它们添加到某个数组中,就可以轻松挂载更多文件夹。该脚本不需要提升权限即可运行。 只需记住,你运行它的文件夹会以读写方式挂载到沙盒中。当我想要运行一个危险命令而不影响我正在操作的仓库中的文件时,我会制作一个临时副本: ``` project > cd .. > git clone project project2 > cd project2 project2 > box fish project2@sandbox > ...some dangerous command... ... project2@sandbox > ^D project2 > cd .. > rm -rf project2 ``` 另一个我最近使用的技巧:我创建了一个只读的 GitHub 个人访问令牌,并将其自动放入沙盒中的 `$GH_TOKEN`。这样,像 `gh pr list` 这样的命令就可以在沙盒中运行,而无需任何写权限。 ## [结论](https://bxt.rs/blog/easy-sandboxing-on-linux-with-bubblewrap/#conclusion) 这绝对不是一个精致的工具,而是一个我几个月来不断添补的脚本。我想分享它,是因为我觉得它相当通用(至少可以在 Fedora 和 [AerynOS](https://aerynos.com/) 上运行),并且避免了其他沙盒方案的许多痛点: - 无需额外设置,只需一条命令 - 无需单独安装发行版,直接使用主机系统的二进制文件和库。因此,在这个沙盒中构建的任何内容都可以在主机上运行 - 无需手动绑定文件夹,直接穿透当前文件夹 - 路径和 UID 与主机一致,不会出现文件权限错乱 - 无需 sudo 一个限制是,我还没能让 [podman](https://podman.io/) 在这个沙盒中运行。我曾短暂尝试过,但总是遇到奇怪的错误。也许需要暴露一些 capabilities;不确定。 这显然也不是为了运行完全不可信代码而设计的防弹沙盒。事实上,如果我无意中留下了一些大漏洞,我也不会太惊讶(如果有,请告诉我)。

相似文章

沙盒化令人抓狂

Lobsters Hottest

一篇技术博客,讨论实现安全沙盒技术的复杂性与挫败感。

浏览器标签中的类Linux内核 - 深入解析BrowserPod架构

Lobsters Hottest

深入解析BrowserPod架构,这是一个基于WebAssembly内核的浏览器内沙箱,完全在客户端运行兼容Linux的应用程序。本文涵盖内核设计、磁盘和网络子系统,以及其在浏览器中运行诸如Claude Code等工具的能力。