--- name: sandboxed-deployment description: Isolate multi-user deployments without root. bwrap sandbox. version: 1.0.0 tags: [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) ```bash 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 不可用: ```bash 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_ ← 用户名需 sanitize([^a-zA-Z0-9_-] → _),防路径穿越 目录 /base/deploy_envs/ag_/{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 + "/")`。 ## 防逃逸验证(每次都要实测,别口头声称) ```bash # 沙箱内执行: 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")` 检测: ```bash # 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//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 内,拒绝 `..` 和绝对路径。