强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bubblewrap 容器入门教程 / 第 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 性能对比

场景纯 DockerDocker + bwrap
启动时间~500ms~550ms
文件系统overlayoverlay + 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 注意事项

⚠️ 重要提醒

  1. Docker 的 --privileged 太宽松:授予所有权能和设备访问,仅用于调试。生产环境应使用精确的 --cap-add

  2. seccomp=unconfined 降低安全性:禁用 Docker 默认的 seccomp 过滤。评估是否真正需要。

  3. 嵌套命名空间有性能开销:每层嵌套都有一定开销,避免不必要的深度嵌套。

  4. 存储驱动兼容性:某些存储驱动(如 overlay2)与嵌套命名空间可能有兼容问题。

  5. CI 环境可能限制命名空间:某些 CI 服务(如 GitHub Actions 的某些 runner)可能限制命名空间操作。

  6. Docker 内的 bwrap 不提供额外安全:如果 Docker 容器已经是 root 且特权模式,bwrap 提供的额外安全价值有限。

  7. 资源限制叠加:Docker 的 cgroup 限制和 bwrap 的 ulimit 可能叠加,注意配置。


9.11 扩展阅读


上一章:第 8 章 - Flatpak 集成 | 下一章:第 10 章 - 最佳实践