使用 Bubblewrap 在 Linux 上轻松实现沙箱
摘要
一篇博客文章,介绍了一种在 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;不确定。
这显然也不是为了运行完全不可信代码而设计的防弹沙盒。事实上,如果我无意中留下了一些大漏洞,我也不会太惊讶(如果有,请告诉我)。
相似文章
Linux 应用沙箱——旧技术的新未来
文章推荐使用成熟的 Firejail 工具,在 Linux 上限制程序的网络、文件系统及硬件访问,无需等待 Wayland 等新显示技术。
沙盒化令人抓狂
一篇技术博客,讨论实现安全沙盒技术的复杂性与挫败感。
自托管的开发沙箱与预览URL(Docker、Go、无K8s)
sandboxed 是一个开源引擎,能将单个 Linux 机器转变为一系列隔离的开发沙箱,配备编码代理和实时预览 URL,支持自托管且易于安装。
Show HN: Z-Jail – 一个130KB的Linux沙箱-C99,具有7层防御且无依赖项
Z-Jail 是一个轻量级Linux沙箱,采用C99编写,拥有七层防御、无外部依赖,且二进制文件极小(约130 KiB),专为CI、CTF和轻量级评估中的安全代码执行而设计。
浏览器标签中的类Linux内核 - 深入解析BrowserPod架构
深入解析BrowserPod架构,这是一个基于WebAssembly内核的浏览器内沙箱,完全在客户端运行兼容Linux的应用程序。本文涵盖内核设计、磁盘和网络子系统,以及其在浏览器中运行诸如Claude Code等工具的能力。