第 9 章 - Docker 中使用
第 9 章:Docker 中使用 Bubblewrap
本章讲解如何在 Docker 容器中使用 Bubblewrap,包括嵌套沙箱配置、测试环境搭建以及在 CI/CD 流水线中的集成方案。
9.1 Docker 中使用 bwrap 的挑战
在 Docker 容器中运行 bwrap 需要解决一些特殊问题:
| 挑战 | 原因 | 解决方案 |
|---|---|---|
| User Namespace 被禁用 | Docker 默认不授予 SYS_ADMIN 权能 | 使用 --privileged 或 --cap-add |
| 嵌套命名空间限制 | 内核对嵌套命名空间有深度限制 | 调整内核参数 |
| AppArmor/SELinux 限制 | Docker 应用了安全策略 | 使用 --security-opt |
| seccomp 限制 | Docker 默认的 seccomp 配置 | 使用 --security-opt seccomp=unconfined |
| 文件系统权限 | overlay 文件系统限制 | 使用 --tmpfs 或 volume |
9.2 基本 Docker 配置
最简单的配置
# Dockerfile.bwrap
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y bubblewrap && \
rm -rf /var/lib/apt/lists/*
# 创建测试脚本
RUN echo '#!/bin/bash\n\
bwrap --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \\\n\
--unshare-pid bash -c "echo Hello from nested sandbox; ps aux"\n\
' > /test-bwrap.sh && chmod +x /test-bwrap.sh
CMD ["/test-bwrap.sh"]
运行容器:
# 需要特权模式或特定权能
docker run --rm --privileged debian-bwrap-test
# 或者使用更精确的权能配置
docker run --rm \
--cap-add SYS_ADMIN \
--cap-add SYS_PTRACE \
--security-opt apparmor=unconfined \
--security-opt seccomp=unconfined \
debian-bwrap-test
需要的权能说明
| 权能 | 用途 | 是否必需 |
|---|---|---|
SYS_ADMIN | 创建命名空间 | 是 |
SYS_PTRACE | 进程跟踪(某些 bwrap 版本需要) | 可能 |
NET_ADMIN | 创建网络命名空间 | 如果需要网络隔离 |
9.3 Dockerfile 最佳实践
生产级 Dockerfile
# Dockerfile.bwrap-ci
FROM ubuntu:22.04
# 安装依赖
RUN apt-get update && \
apt-get install -y \
bubblewrap \
build-essential \
git \
python3 \
&& \
rm -rf /var/lib/apt/lists/*
# 创建非 root 用户
RUN useradd -m -s /bin/bash sandbox-user
# 创建 bwrap wrapper 脚本
COPY <<'EOF' /usr/local/bin/sandbox-run
#!/bin/bash
set -euo pipefail
# 检查 bwrap 是否可用
if ! command -v bwrap &>/dev/null; then
echo "Error: bwrap not found" >&2
exit 1
fi
# 默认沙箱参数
BWRAP_ARGS=(
--ro-bind / /
--dev /dev
--proc /proc
--tmpfs /tmp
--unshare-pid
--unshare-uts
--hostname ci-sandbox
--die-with-parent
--new-session
--cap-drop ALL
)
# 如果不需要网络,取消注释下一行
# BWRAP_ARGS+=(--unshare-net)
exec bwrap "${BWRAP_ARGS[@]}" "$@"
EOF
RUN chmod +x /usr/local/bin/sandbox-run
# 切换到非 root 用户
USER sandbox-user
WORKDIR /home/sandbox-user
# 默认在沙箱中运行 bash
CMD ["sandbox-run", "bash"]
构建和使用
# 构建镜像
docker build -t bwrap-ci -f Dockerfile.bwrap.ci .
# 运行交互式容器
docker run --rm -it \
--cap-add SYS_ADMIN \
--security-opt apparmor=unconfined \
--security-opt seccomp=unconfined \
bwrap-ci
# 在容器内测试
sandbox-run echo "Hello from nested sandbox"
sandbox-run bash -c 'ps aux; hostname'
9.4 嵌套沙箱配置
Docker + bwrap 嵌套架构
宿主系统
└── Docker 容器 (Dockerfile)
└── bwrap 沙箱 (sandbox-run)
└── 应用进程
完整的嵌套配置
# docker-compose.yml
version: '3.8'
services:
bwrap-sandbox:
build:
context: .
dockerfile: Dockerfile.bwrap
cap_add:
- SYS_ADMIN
security_opt:
- apparmor=unconfined
- seccomp=unconfined
tmpfs:
- /tmp:size=256M
volumes:
- ./workspace:/workspace:ro
command: >
bwrap
--ro-bind / /
--bind /workspace /workspace
--dev /dev
--proc /proc
--tmpfs /tmp
--unshare-pid
--unshare-uts
--hostname nested-sandbox
--die-with-parent
bash -c 'cd /workspace && make test'
9.5 CI/CD 集成
GitHub Actions
# .github/workflows/sandbox-test.yml
name: Sandbox Test
on: [push, pull_request]
jobs:
test-in-sandbox:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install bubblewrap
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap
# 启用 user namespace
sudo sysctl kernel.unprivileged_userns_clone=1
- name: Run tests in sandbox
run: |
bwrap \
--ro-bind / / \
--bind "$GITHUB_WORKSPACE" /workspace \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--unshare-pid \
--unshare-uts \
--hostname ci-sandbox \
--die-with-parent \
--chdir /workspace \
bash -c '
echo "=== Running in sandbox ==="
echo "Hostname: $(hostname)"
echo "PID: $$"
ls -la
make test
'
- name: Build in isolated sandbox
run: |
bwrap \
--ro-bind / / \
--bind "$GITHUB_WORKSPACE" /workspace \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--tmpfs /build \
--unshare-all \
--die-with-parent \
bash -c '
cd /workspace
mkdir -p /build/output
make DESTDIR=/build/output install
echo "Build artifacts:"
ls -la /build/output/
'
GitLab CI
# .gitlab-ci.yml
stages:
- test
sandbox-test:
stage: test
image: ubuntu:22.04
before_script:
- apt-get update
- apt-get install -y bubblewrap build-essential
- sysctl kernel.unprivileged_userns_clone=1
script:
- |
bwrap \
--ro-bind / / \
--bind "$(pwd)" /workspace \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--unshare-all \
--die-with-parent \
--chdir /workspace \
bash -c 'make test'
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent {
docker {
image 'ubuntu:22.04'
args '--cap-add SYS_ADMIN --security-opt apparmor=unconfined --security-opt seccomp=unconfined'
}
}
stages {
stage('Setup') {
steps {
sh 'apt-get update && apt-get install -y bubblewrap build-essential'
}
}
stage('Test in Sandbox') {
steps {
sh '''
bwrap \
--ro-bind / / \
--bind "$(pwd)" /workspace \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--unshare-all \
--die-with-parent \
--chdir /workspace \
bash -c 'make test'
'''
}
}
}
}
9.6 测试不可信代码
在线评测系统 (OJ)
#!/bin/bash
# run-in-sandbox.sh - 在沙箱中安全执行用户代码
set -euo pipefail
CODE_FILE="$1"
INPUT_FILE="${2:-/dev/null}"
TIMEOUT="${3:-5}"
MEMORY_LIMIT="${4:-256}"
# 检查输入
if [ ! -f "$CODE_FILE" ]; then
echo "Error: Code file not found: $CODE_FILE" >&2
exit 1
fi
# 创建临时工作目录
WORK_DIR=$(mktemp -d)
trap "rm -rf $WORK_DIR" EXIT
# 复制代码到工作目录
cp "$CODE_FILE" "$WORK_DIR/code"
if [ -f "$INPUT_FILE" ]; then
cp "$INPUT_FILE" "$WORK_DIR/input"
fi
# 在沙箱中执行
timeout "$TIMEOUT" bwrap \
--ro-bind / / \
--bind "$WORK_DIR" /workspace \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--size "$MEMORY_LIMIT" \
--unshare-all \
--die-with-parent \
--new-session \
--cap-drop ALL \
bash -c '
cd /workspace
# 编译(如果是 C/C++)
if [[ code == *.c ]]; then
gcc -o program code 2> compile_error && \
timeout 2 ./program < input > output 2>&1
elif [[ code == *.py ]]; then
timeout 2 python3 code < input > output 2>&1
fi
# 输出结果
if [ -f output ]; then
cat output
elif [ -f compile_error ]; then
echo "Compilation Error:"
cat compile_error
fi
' 2>&1 || echo "Time Limit Exceeded"
使用:
# 测试 C 代码
cat > /tmp/solution.c << 'EOF'
#include <stdio.h>
int main() {
int n;
scanf("%d", &n);
printf("Answer: %d\n", n * 2);
return 0;
}
EOF
echo "5" > /tmp/input.txt
./run-in-sandbox.sh /tmp/solution.c /tmp/input.txt 5 128
# Answer: 10
9.7 Docker 内的高级 bwrap 用法
无特权模式
# 使用 user namespace 的无特权 bwrap
FROM fedora:39
RUN dnf install -y bubblewrap && \
dnf clean all
# 启用 user namespace
RUN echo "user.max_user_namespaces=28633" > /etc/sysctl.d/99-userns.conf
# 创建非 root 用户
RUN useradd -m testuser
USER testuser
WORKDIR /home/testuser
CMD ["bwrap", "--ro-bind", "/", "/", "--unshare-user", "--uid", "0", "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp", "bash"]
调试容器
# 进入带 bwrap 的调试容器
docker run --rm -it \
--cap-add SYS_ADMIN \
--security-opt apparmor=unconfined \
--security-opt seccomp=unconfined \
--pid=host \
ubuntu:22.04 \
bash -c '
apt-get update && apt-get install -y bubblewrap
echo "=== Test 1: Basic sandbox ==="
bwrap --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \
bash -c "echo PID: \$$; hostname"
echo ""
echo "=== Test 2: Isolated sandbox ==="
bwrap --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \
--unshare-all --hostname debug-sandbox \
bash -c "echo PID: \$$; hostname; ps aux | head -5"
echo ""
echo "=== Test 3: File system isolation ==="
bwrap --ro-bind / / --tmpfs /tmp --dev /dev --proc /proc \
bash -c "ls /tmp; echo test > /tmp/foo; ls /tmp"
echo "Back in host, /tmp contents:"
ls /tmp
'
9.8 性能考量
Docker + bwrap 性能对比
| 场景 | 纯 Docker | Docker + bwrap |
|---|---|---|
| 启动时间 | ~500ms | ~550ms |
| 文件系统 | overlay | overlay + bind mount |
| 内存开销 | ~10MB | ~12MB |
| 进程创建 | 正常 | 略慢(嵌套命名空间) |
优化建议
# 1. 避免不必要的嵌套命名空间
# 只分离需要的命名空间
bwrap --ro-bind / / --tmpfs /tmp ... # 不要使用 --unshare-all
# 2. 减少绑定挂载数量
# 使用 --ro-bind / / 而不是逐个绑定
bwrap --ro-bind / / ... # 而不是 --ro-bind /usr /usr --ro-bind /lib /lib ...
# 3. 使用 --die-with-parent
# 防止孤儿进程
bwrap --die-with-parent ...
9.9 Podman 中使用 bwrap
Podman 与 Docker API 兼容,但有一些特殊配置:
# Podman rootless 模式天然支持 user namespace
podman run --rm -it \
--cap-add SYS_ADMIN \
ubuntu:22.04 \
bash -c '
apt-get update && apt-get install -y bubblewrap
bwrap --ro-bind / / --unshare-user --uid 0 --dev /dev --proc /proc --tmpfs /tmp \
bash -c "id; echo PID: $$"
'
# 或者使用 Podman 的 --userns=keep-id 保持 UID 映射
podman run --rm -it \
--userns=keep-id \
--cap-add SYS_ADMIN \
ubuntu:22.04 \
bash -c '
apt-get update && apt-get install -y bubblewrap
bwrap --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp bash
'
9.10 注意事项
⚠️ 重要提醒
Docker 的
--privileged太宽松:授予所有权能和设备访问,仅用于调试。生产环境应使用精确的--cap-add。seccomp=unconfined 降低安全性:禁用 Docker 默认的 seccomp 过滤。评估是否真正需要。
嵌套命名空间有性能开销:每层嵌套都有一定开销,避免不必要的深度嵌套。
存储驱动兼容性:某些存储驱动(如
overlay2)与嵌套命名空间可能有兼容问题。CI 环境可能限制命名空间:某些 CI 服务(如 GitHub Actions 的某些 runner)可能限制命名空间操作。
Docker 内的 bwrap 不提供额外安全:如果 Docker 容器已经是 root 且特权模式,bwrap 提供的额外安全价值有限。
资源限制叠加:Docker 的 cgroup 限制和 bwrap 的 ulimit 可能叠加,注意配置。
9.11 扩展阅读
- Docker Security
- Podman Rootless
- GitHub Actions Security
- Running Containers without Root
- Linux Containers in 500 Lines of Code
上一章:第 8 章 - Flatpak 集成 | 下一章:第 10 章 - 最佳实践