Bubblewrap 容器入门教程 / 第 5 章 - 文件系统隔离
第 5 章:文件系统隔离
本章深入讲解 Bubblewrap 中文件系统隔离的各种技术,包括绑定挂载、只读挂载、tmpfs、proc 文件系统和设备文件系统,以及如何构建精细的文件系统布局。
5.1 文件系统隔离基础
Bubblewrap 的文件系统隔离基于 Linux 的 Mount Namespace。通过精心设计挂载点布局,可以精确控制沙箱进程能访问哪些文件,以及以什么权限访问。
隔离策略概览
| 策略 | 说明 | bwrap 参数 |
|---|---|---|
| 完全只读 | 整个文件系统只读 | --ro-bind / / |
| 选择性可写 | 大部分只读,特定目录可写 | --ro-bind / / + --bind / --tmpfs |
| 最小可见 | 只暴露必要的文件 | 逐个 --ro-bind 需要的目录 |
| 内存文件系统 | 完全在 tmpfs 中运行 | --tmpfs / + --ro-bind 必要的 |
文件系统布局示例
沙箱文件系统树:
/
├── bin/ → ro-bind /bin (只读)
├── dev/ → 最小 dev (null, zero, random, urandom)
├── etc/ → ro-bind /etc (只读)
├── home/ → tmpfs (可写, 内存)
│ └── user/ → tmpfs (可写, 内存)
├── lib/ → ro-bind /lib (只读)
├── lib64/ → ro-bind /lib64 (只读)
├── proc/ → proc (沙箱 PID 视图)
├── run/ → tmpfs (可写, 内存)
├── sbin/ → ro-bind /sbin (只读)
├── tmp/ → tmpfs (可写, 内存)
├── usr/ → ro-bind /usr (只读)
└── var/ → tmpfs (可写, 内存)
5.2 绑定挂载(Bind Mount)
绑定挂载是 Linux mount 系统调用的一个特性,允许将一个目录或文件映射到另一个路径。Bubblewrap 提供两种绑定挂载:
只读绑定(--ro-bind)
# 将宿主的 /usr 以只读方式挂载到沙箱的 /usr
bwrap \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /bin /bin \
--tmpfs / \
--dev /dev \
--proc /proc \
bash
# 在沙箱内尝试写入——被拒绝
touch /usr/test.txt
# touch: cannot touch '/usr/test.txt': Read-only file system
echo "test" > /etc/test.conf
# bash: /etc/test.conf: Read-only file system
读写绑定(--bind)
# 将宿主的 ~/documents 以读写方式挂载到沙箱
bwrap \
--ro-bind / / \
--bind ~/documents /documents \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
bash
# 在沙箱内写入——写回到宿主
echo "from sandbox" > /documents/notes.txt
exit
# 在宿主验证
cat ~/documents/notes.txt
# from sandbox
文件级绑定
绑定挂载不只限于目录,也可以绑定单个文件:
# 只暴露特定的配置文件
bwrap \
--ro-bind / / \
--tmpfs /etc \
--ro-bind /etc/passwd /etc/passwd \
--ro-bind /etc/group /etc/group \
--ro-bind /etc/hosts /etc/hosts \
--dev /dev \
--proc /proc \
bash
# 现在沙箱的 /etc 只有 passwd、group、hosts 三个文件
ls /etc/
# group hosts passwd
绑定挂载的传播属性
# 使用 --bind 创建私有绑定(默认)
bwrap \
--ro-bind / / \
--bind ~/data /data \
bash
# /data 的挂载事件不会传播到宿主
# 从沙箱内挂载额外的文件系统不会影响宿主
bwrap \
--ro-bind / / \
--bind ~/data /data \
bash -c '
# 尝试在沙箱内挂载 tmpfs
mount -t tmpfs tmpfs /data/tmp 2>&1 || echo "Mount failed"
'
5.3 tmpfs 文件系统
tmpfs 是一种基于内存的文件系统,数据存储在 RAM 中,不持久化到磁盘。
基本用法
# 创建 tmpfs 挂载点
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--tmpfs /var/tmp \
--tmpfs /run \
--dev /dev \
--proc /proc \
bash
# 在沙箱内写入 tmpfs
echo "temporary data" > /tmp/data.txt
cat /tmp/data.txt
# temporary data
# 查看 tmpfs 使用情况
df -h /tmp
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 16G 0 16G 0% /tmp
限制 tmpfs 大小
# 使用 --size 限制 tmpfs 大小(单位 MB)
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--size 64 \
--dev /dev \
--proc /proc \
bash -c '
df -h /tmp
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 64M 0 64M 0% /tmp
# 测试写入限制
dd if=/dev/zero of=/tmp/bigfile bs=1M count=70 2>&1
# dd: error writing '/tmp/bigfile': No space left on device
'
tmpfs 选项
# tmpfs 支持多种挂载选项
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--size 128 \
--dev /dev \
--proc /proc \
bash
# 注意:bwrap 的 --size 选项直接传递给 tmpfs 的 size= 挂载选项
tmpfs 的典型使用场景
| 挂载点 | 用途 | 推荐大小 |
|---|---|---|
/tmp | 临时文件 | 64-512 MB |
/var/tmp | 持久临时文件 | 64-256 MB |
/run | 运行时数据(PID 文件等) | 16-64 MB |
/home/user | 用户主目录 | 128-1024 MB |
/home/user/.cache | 缓存目录 | 256-2048 MB |
/home/user/.config | 配置目录 | 32-128 MB |
5.4 Proc 文件系统
/proc 是一个虚拟文件系统,提供内核和进程信息的接口。
基本用法
# 为 PID 命名空间挂载 proc
bwrap \
--ro-bind / / \
--unshare-pid \
--proc /proc \
--dev /dev \
bash
# /proc 只显示沙箱内的进程
ls /proc/*/cmdline 2>/dev/null | while read f; do
pid=$(echo "$f" | cut -d'/' -f3)
echo "PID $pid: $(cat "$f" 2>/dev/null | tr '\0' ' ')"
done
# PID 1: bash
没有 --proc 的影响
# 如果不挂载 /proc,很多工具会出错
bwrap \
--ro-bind / / \
--unshare-pid \
--dev /dev \
bash -c 'ps aux'
# error: /proc must be mounted
# No /proc directory? Check your bwrap command line.
/proc 中的安全信息
# /proc 包含大量系统信息,沙箱中需要谨慎暴露
bwrap \
--ro-bind / / \
--proc /proc \
--dev /dev \
bash -c '
# 内核版本
cat /proc/version
# Linux version 6.1.0 ...
# CPU 信息
cat /proc/cpuinfo | head -10
# 内存信息
cat /proc/meminfo | head -5
'
5.5 设备文件系统(dev)
--dev /dev 创建一个最小的设备文件系统,只包含必要的设备文件。
创建的设备文件
bwrap \
--ro-bind / / \
--dev /dev \
bash -c 'ls -la /dev/'
| 设备 | 用途 | 权限 |
|---|---|---|
/dev/null | 丢弃所有写入,读取返回 EOF | crw-rw-rw- |
/dev/zero | 读取返回零字节 | crw-rw-rw- |
/dev/full | 写入返回 ENOSPC | crw-rw-rw- |
/dev/random | 随机数(阻塞) | crw-rw-rw- |
/dev/urandom | 随机数(非阻塞) | crw-rw-rw- |
/dev/fd | → /proc/self/fd 符号链接 | lrwxrwxrwx |
/dev/ptmx | 伪终端主设备 | crw-rw-rw- |
/dev/pts/ | 伪终端从设备目录 | drwxr-xr-x |
/dev/shm/ | 共享内存目录 | drwxrwxrwt |
/dev/stdin | → /proc/self/fd/0 | lrwxrwxrwx |
/dev/stdout | → /proc/self/fd/1 | lrwxrwxrwx |
/dev/stderr | → /proc/self/fd/2 | lrwxrwxrwx |
不使用 --dev 的影响
# 不挂载 /dev,很多程序会报错
bwrap \
--ro-bind / / \
--proc /proc \
bash -c 'ls /dev/null'
# ls: cannot access '/dev/null': No such file or directory
# 程序通常需要 /dev/null
bwrap \
--ro-bind / / \
--proc /proc \
bash -c 'echo test > /dev/null'
# bash: /dev/null: No such file or directory
5.6 符号链接(--symlink)
# 创建符号链接
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--symlink /usr/lib /lib \
--symlink /usr/lib64 /lib64 \
--symlink /usr/bin /bin \
--symlink /usr/sbin /sbin \
--dev /dev \
--proc /proc \
bash
# 验证符号链接
ls -la /lib
# lrwxrwxrwx 1 root root 8 ... /lib -> /usr/lib
实际应用:简化文件系统布局
# 为某些需要特定路径的程序创建兼容层
bwrap \
--ro-bind / / \
--tmpfs /opt \
--dir /opt/myapp \
--bind ~/myapp/lib /opt/myapp/lib \
--symlink /opt/myapp/lib /usr/local/lib/myapp \
--dev /dev \
--proc /proc \
bash
5.7 创建目录(--dir)
# 在沙箱内创建目录
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--dir /tmp/workspace \
--dir /tmp/output \
--dir /tmp/cache \
--dev /dev \
--proc /proc \
bash -c '
ls -la /tmp/
# drwxr-xr-x 2 root root 40 ... cache
# drwxr-xr-x 2 root root 40 ... output
# drwxr-xr-x 2 root root 40 ... workspace
'
5.8 从文件描述符创建文件(--file)
# 使用 heredoc 创建配置文件
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
--file 0 /tmp/config.ini << 'EOF'
[settings]
debug = true
output_dir = /tmp/output
max_threads = 4
EOF
bash -c 'cat /tmp/config.ini'
# [settings]
# debug = true
# output_dir = /tmp/output
# max_threads = 4
实际应用:注入配置文件
# 将宿主的配置文件修改后注入沙箱
cat > /tmp/sandbox-bashrc << 'EOF'
# Sandbox bashrc
export PS1="[sandbox] \u@\h:\w\$ "
alias ll='ls -la'
echo "Welcome to the sandbox!"
EOF
bwrap \
--ro-bind / / \
--tmpfs /home/sandbox \
--file 0 /home/sandbox/.bashrc < /tmp/sandbox-bashrc \
--dir /home/sandbox \
--clearenv \
--setenv HOME /home/sandbox \
--setenv TERM xterm-256color \
--setenv PATH /usr/local/bin:/usr/bin:/bin \
--chdir /home/sandbox \
--dev /dev \
--proc /proc \
bash
# Welcome to the sandbox!
# [sandbox] user@hostname:~$
5.9 高级文件系统布局
最小化可见文件系统
只暴露程序运行所需的最少文件:
# 创建一个只包含必要文件的最小沙箱
bwrap \
--tmpfs / \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind /sbin /sbin \
--ro-bind /etc/passwd /etc/passwd \
--ro-bind /etc/group /etc/group \
--ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--ro-bind /etc/hosts /etc/hosts \
--ro-bind /etc/localtime /etc/localtime \
--ro-bind /etc/ssl /etc/ssl \
--symlink /usr/lib /lib \
--symlink /usr/lib64 /lib64 \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--tmpfs /home \
--tmpfs /var \
--tmpfs /run \
bash
# 现在沙箱内只能看到:
ls /
# bin dev etc home lib lib64 proc run sbin tmp usr var
选择性暴露用户目录
# 只暴露特定的项目目录,隐藏其他所有个人文件
bwrap \
--ro-bind / / \
--tmpfs /home/user \
--bind ~/projects/current /home/user/project \
--bind ~/documents/shared /home/user/shared \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
bash -c '
ls /home/user/
# project shared
# 看不到其他个人文件
ls /home/user/.ssh 2>/dev/null || echo "No .ssh directory"
# No .ssh directory
'
为特定应用定制文件系统
# Python 开发环境沙箱
bwrap \
--ro-bind / / \
--bind ~/projects/python-app /app \
--tmpfs /home/dev \
--ro-bind ~/projects/python-app/requirements.txt /tmp/requirements.txt \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
--unshare-pid \
--chdir /app \
bash -c '
python3 --version
pip3 install -r /tmp/requirements.txt --user --target /home/dev/libs 2>/dev/null
PYTHONPATH=/home/dev/libs python3 main.py
'
5.10 防止文件系统泄露
隐藏敏感文件
# 隐藏 SSH 密钥和其他敏感文件
bwrap \
--ro-bind / / \
--tmpfs /home/user/.ssh \
--tmpfs /home/user/.gnupg \
--tmpfs /home/user/.aws \
--tmpfs /home/user/.kube \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
bash
# 在沙箱内
ls ~/.ssh/
# (空)
ls ~/.aws/
# (空)
只读保护系统关键文件
# 确保系统文件不可修改
bwrap \
--bind / / \
--remount-ro /etc \
--remount-ro /usr \
--remount-ro /boot \
--remount-ro /lib \
--remount-ro /lib64 \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
bash
# 尝试修改系统文件——被拒绝
echo "malicious" > /etc/passwd
# bash: /etc/passwd: Read-only file system
5.11 文件系统权限总结表
| 挂载方式 | 可读 | 可写 | 数据持久化 | 数据来源 |
|---|---|---|---|---|
--ro-bind SRC DST | ✅ | ❌ | 宿主文件同步 | 宿主目录 |
--bind SRC DST | ✅ | ✅ | 宿主文件同步 | 宿主目录 |
--tmpfs DST | ✅ | ✅ | ❌ (退出即丢失) | 内存 |
--dev DST | ✅ | ✅* | ❌ (退出即丢失) | 最小设备 |
--proc DST | ✅ | ✅* | 内核虚拟 | 内核 |
--symlink SRC DST | ✅ | ❌ | — | 指向 SRC |
--dir DST | ✅ | ✅ | 取决于父挂载点 | — |
--file FD DST | ✅ | ❌ | 取决于父挂载点 | 文件描述符 |
*
/dev和/proc的可写性取决于具体文件和内核版本。
5.12 注意事项
⚠️ 重要提醒
参数顺序决定最终布局:后面的挂载会覆盖前面的。始终将
--ro-bind / /放在前面,特定目录的覆盖放在后面。不要暴露
/proc/kallsyms和/proc/kcore:这些文件泄露内核符号和内存信息。bwrap 的--proc会正确处理这些。--dev /dev不包含磁盘设备:沙箱无法直接访问磁盘(如/dev/sda)。tmpfs 占用内存:大文件写入 tmpfs 会消耗物理内存,注意使用
--size限制。绑定挂载的修改是真实的:使用
--bind的修改会写回宿主文件系统,谨慎使用。SELinux/AppArmor 可能额外限制:即使命名空间允许,MAC 策略也可能拒绝操作。
/proc在 PID 命名空间中尤为重要:没有正确的--proc,许多工具(ps、top、lsof)将无法正常工作。
5.13 扩展阅读
上一章:第 4 章 - 命名空间详解 | 下一章:第 6 章 - 网络隔离