7.6 KiB
Raw Blame History

name description version tags
sandboxed-deployment Isolate multi-user deployments without root. bwrap sandbox. 1.0.0
sandbox
bwrap
deployment
isolation
zero-root
multi-tenant
security

沙箱化部署隔离(零 root)

为多用户提供隔离的部署/运行环境,全程零 root。核心模式:逻辑账号(目录 + DB 映射,不建真实 Linux 账号)+ bwrap 沙箱(unprivileged user namespace)。

先问清楚需求本质(决定要不要 root)

动手前必须先确认:隔离的目的是「给人 SSH 登录」还是「给部署进程一个隔离环境」?

  • 要真实 Linux 账号(人 SSH 登录 / 独立 uid / 磁盘配额)→ 需要 root useradd。但 root 能力应放在独立最小权限服务里(只暴露固定动作),不要给 web 服务 sudo 免密。
  • 只要隔离部署(部署进程在隔离环境里跑)→ 逻辑隔离:目录 + DB 记录 + bwrap,全程零 root。

为什么不要给 web 服务 sudo 免密

给运行 LLM 生成命令的 web 服务 sudo 免密(哪怕命令白名单)不安全:

  • 一旦被 RCE 或 prompt 注入,sudo = 直接 root。
  • 白名单可被绕过:useradd -o -u 0 attacker 直接造 uid=0 账号;usermod/chown/setfacl 链式提权。

替代:逻辑隔离(零 root)或独立 root systemd 服务(unix socket 调用,只接受「创建账号/删账号」固定动作,不接受任意命令)。

bwrap 零 root 获取(无需 apt install,无需 sudo)

apt download bubblewrap          # 普通用户可下载 .deb,无需 root
dpkg -x bubblewrap*.deb /tmp/x   # 解包提取二进制,无需 root
cp /tmp/x/usr/bin/bwrap /path/to/bin/bwrap

动态库(libcap/libseccomp)系统通常已自带(ldd bwrap 验证无 "not found")。

前提检查:unprivileged user namespace 必须启用,否则 bwrap 不可用:

cat /proc/sys/kernel/unprivileged_userns_clone   # =1 表示启用
unshare -Ur echo 'userns OK'                      # 能输出则可用

bwrap 沙箱参数模板

隔离 user/pid/ipc/uts namespace(保留网络,部署应用通常要绑定端口/访问外部),只读挂载系统目录,可写挂载用户部署目录,tmpfs /tmp。完整模板见 references/bwrap-template.md。

关键点:

  • 用 --unshare-user --unshare-pid --unshare-ipc --unshare-uts(不是 --unshare-all,后者会 --unshare-net 把网络也隔离掉)。
  • --setenv HOME /home 必须加:否则沙箱内 HOME 继承宿主机路径(如 /d/pipeline),而该路径在沙箱内不存在,应用 cd $HOME 会失败。
  • 执行用 create_subprocess_exec(*cmd) 传列表(非 shell 字符串),防命令注入。

逻辑隔离账号模式

账号名  ag_<username>   ← 用户名需 sanitize([^a-zA-Z0-9_-] → _),防路径穿越
目录    /base/deploy_envs/ag_<username>/{home,workspace,data,logs}
DB 记录  sd_deploy_accounts(id, user_id, account_name, deploy_dir, status, sandbox_config)
  • 「免密访问」在逻辑隔离下 = 部署进程直接文件系统读写(无 SSH/密码),天然免密。
  • 「超级用户权限自动开启」在零 root 下 = bwrap user namespace 内 root-like 映射(沙箱内 uid 0,映射回宿主机是普通用户)。
  • 文件读写/列表必须做路径穿越校验:拒绝绝对路径、拒绝 ..,并 os.path.abspath 后校验 startswith(deploy_dir + "/")。

防逃逸验证(每次都要实测,别口头声称)

# 沙箱内执行:
touch /etc/test_escape   # → "Read-only file system"(只读系统目录生效,防逃逸)
echo $$                  # → PID 从 1 开始(PID namespace 生效)
ls /                     # → 只显示挂载的目录(文件系统隔离生效)
ls /home                 # → 显示部署目录内容(可写目录正确挂载)
sudo -n id               # → "no new privileges" 报错(sudo 提权被自动阻断)

bwrap 自动阻断 sudo 提权(关键安全机制):--unshare-user 的 user namespace 会设置 no_new_privileges 标志,setuid 程序(sudo 等)无法提权。实测沙箱内 sudo 直接报:

sudo: The "no new privileges" flag is set, which prevents sudo from running as root.

所以即使宿主用户(如 pipeline)在 sudo 组里,沙箱内进程也无法通过 sudo 提权。给陌生用户开 Wterm/终端时,这是防提权的第一道自动防线,无需额外配置。

bwrap 一键部署集成

把 bwrap 获取放进 build.sh(零 root),start.sh 把 bin 目录加到 PATH,服务代码用 shutil.which("bwrap") 检测:

# build.sh:系统已有则复用,否则 apt download + dpkg -x 到 $cdir/bin/
if [ ! -x "$cdir/bin/bwrap" ]; then
    if command -v bwrap >/dev/null 2>&1; then
        cp "$(command -v bwrap)" "$cdir/bin/bwrap"
    else
        tmpd=$(mktemp -d)
        ( cd "$tmpd" && apt download bubblewrap >/dev/null 2>&1 && dpkg -x bubblewrap*.deb . && cp usr/bin/bwrap "$cdir/bin/bwrap" )
        rm -rf "$tmpd"
    fi
fi
# start.sh:export PATH="$WORKDIR/bin:$PATH"(让 python 进程 shutil.which 能找到)

远程主机沙箱(免密 SSH + 远程 bwrap)

沙箱可切到远程主机:ssh -i key user@host 在远程同样跑 bwrap。安全校验必须过一遍(audit 踩过的坑):

  • 每机构独立 key:不要所有机构共用一个私钥(泄露一个 = 全崩,且 pipeline 是跑 LLM 命令的高风险用户)。ssh-keygen -t ed25519 -N "" -f ~/.ssh/org_keys/<org_id>/id_ed25519 每机构一把。
  • host key 校验:-o StrictHostKeyChecking=accept-new(首次记录 host key 后严格校验,防 MITM),不要 =no——=no 等于把机构数据 rsync 给任意伪装主机。
  • remote_dir 禁止系统目录:迁移用 rsync --delete,remote_dir 设成 / 或 /etc 会删远程系统文件。黑名单 + 前缀检查;注意 .rstrip("/") 会把 "/" 变成空串绕过黑名单,要先判空。
  • remote_key_path 限制 ~/.ssh/ 下:允许任意绝对路径 = 能读服务器上任意私钥(git deploy key 等)去认证别的主机。
  • host/user/port 正则白名单:防 evil.com;rm -rf、root;rm 注入;port 要 int() 加 try/except(传 "abc" 会 500)+ 范围 1-65535。
  • 机构级配置需权限校验:org 级 remote_host 仅 owner.superuser 可设(查 userrole),否则任何登录用户都能把整个机构内容迁移到攻击者服务器。
  • 迁移失败状态一致:先迁移成功再写 DB,失败不回写 mode(否则 mode 已切但内容没迁,状态分裂)。

SSH 命令构造:用 create_subprocess_exec(*["ssh", ...]) 列表传参(非 shell 拼接);远程 bwrap 路径探测 BW=$(command -v bwrap || echo "$HOME/bin/bwrap"); exec "$BW" ...(远程 SSH 会话 PATH 不含自定义 bin)。

Pitfalls

  • 不要 --unshare-all:它会同时 --unshare-net,隔离网络后 web 应用无法监听端口/访问外部,部署应用基本都要网络。
  • --setenv HOME /home 必加:漏了会导致沙箱内 HOME 指向不存在的宿主机路径。
  • bwrap 未安装时降级:代码里 BWRAP = shutil.which("bwrap") 为 None 时降级为「目录 + chdir」弱隔离(sandbox: false 标记),不要直接报错。多路径兜底:PATH → BWRAP_PATH 环境变量 → 已知安装位置。
  • 账号名必须 sanitize:username 可能含中文/特殊字符,目录名只保留 [a-zA-Z0-9_-],防路径穿越。
  • 路径穿越校验:所有文件读写/列表操作都要 abspath 后校验在 deploy_dir 内,拒绝 .. 和绝对路径。