ClouisleClouisle

代码沙箱

在隔离的 Bubblewrap 命名空间中安全运行 Python 与 JavaScript 代码

代码沙箱是 Clouisle 的安全隔离执行环境,用于在工作流、Agent 和工具中运行用户提供的 Python 与 JavaScript 代码。沙箱任务进入专用 Celery Worker(独立消费 sandbox 队列,与主 Worker 的四个队列分开),每个任务在独立的 Bubblewrap 用户+挂载命名空间中执行,并由超时、输出限制与磁盘检查约束失控任务。

官方部署的 sandbox-worker 以 root(user: "0" / runAsUser: 0)运行,并在运行时默认 cap 集之上叠加 CAP_SYS_ADMIN,同时使用 seccomp=unconfined(保留 no-new-privileges)——它不是 rootless 容器。隔离来自每个任务的 Bubblewrap 用户+挂载命名空间,而不是 Worker 自身被降权;逃逸后果与加固方式见下文「加固:收敛 Worker 的 CAP_SYS_ADMIN」。

隔离架构与 Bubblewrap 挂载布局

Agent/工作流 → API → Celery 队列 (sandbox) → 沙箱 Worker
                                                  ↓
                                         Bubblewrap 进程
                                                  ↓
                              /workspace → 当前任务/会话目录

当开启文件系统隔离(SANDBOX_FILESYSTEM_ISOLATION_ENABLED=true)时,沙箱启动器将命令包裹在 bwrap 进程中执行:

bwrap \
  --unshare-user --unshare-pid --unshare-ipc --unshare-uts \
  --die-with-parent --hostname clouisle-sandbox \
  --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /lib /lib --ro-bind /etc /etc \
  --dir /proc --dev /dev \
  --bind <host_job_dir> /workspace \
  --bind <host_job_dir>/tmp /tmp \
  --chdir /workspace \
  -- <command> <args...>
  1. 命名空间与生命周期隔离:
    • 多重隔离:--unshare-user、--unshare-pid、--unshare-ipc、--unshare-uts 创建独立的用户、进程树(映射为 PID 1)、IPC 通信域与主机名(clouisle-sandbox)。
    • 进程级联回收:--die-with-parent 保证 Worker 主管进程退出或超时后,沙箱内所有衍生子进程被内核立即回收。
  2. 只读系统根与工作目录映射 (RUNTIME_READONLY_ROOTS):
    • 系统目录只读:/usr、/bin、/sbin、/lib、/lib64、/etc 通过 --ro-bind 只读挂载,防止修改系统二进制、Python site-packages 或 OS 配置。
    • 独立工作空间:宿主任务目录映射为标准的 /workspace,内部配备独立的 /tmp 子挂载(--bind <workspace>/tmp /tmp),隔离临时文件。
    • 逻辑路径自动重写:_logical_workspace_path 与 _map_workspace_value 自动将任务参数和环境变量中的物理路径重写为 /workspace/...。
  3. 运行时与中日韩(CJK)字体支持:
    • 预置运行时:预装 Python 3.13(基于 uv 管理)与 Node.js 22。
    • CJK 字体支持:clouisle-sandbox-worker 镜像预装 fontconfig、fonts-wqy-zenhei 与 fonts-wqy-microhei 并刷新字体缓存;/usr/share/fonts 通过只读挂载暴露给沙箱,使用 matplotlib、seaborn、Pillow 生成图表时可正常渲染中日韩字符,杜绝方块乱码(豆腐块)。

支持的运行时

运行时基础环境
PythonPython 3.13,包含标准库和任务配置的依赖
JavaScriptNode.js 22,包含核心模块和任务配置的依赖

使用场景

  • 代码工具:在 管理后台 > 功能 > 代码 中创建可复用的代码工具,可安装精确固定版本的包并声明 /workspace 下的产物,保存后被 Agent 和工作流调用(见 自定义 HTTP 与代码工具)。
  • 工作流代码节点:在工作流图中直接嵌入代码,接收输入变量并将结果返回给下游节点(见 工作流节点)。
  • Agent 级执行:Agent 通过函数调用触发代码工具,由 LLM 根据任务需要决定何时运行代码。

执行边界

沙箱任务限制由 SandboxLimits 统一定义,实际生效值取「任务配置」与「调用侧超时」中先到的一个:

限制默认值允许范围说明
timeout_seconds301-600单次任务的执行超时(秒);超时后按进程组 SIGTERM → SIGKILL 回收
disk_mb10240-102400(并受 SANDBOX_MAX_DISK_MB 默认 8192 约束)工作空间磁盘配额
max_stdout_kb25616-8192标准输出上限,超出部分截断并追加 ...<truncated>
max_stderr_kb25616-8192标准错误上限,同上

调用侧还有一层独立的超时:直接执行接口 POST /api/v1/tools/execute-code 的 timeout 默认 30、范围 1-60;Agent 对话中调用代码工具时,外层上限来自 STREAM_TOOL_TIMEOUT_CODE(默认 60 秒)。已保存工具的 limits.timeout_seconds 优先于调用侧传入值(只在配置缺省时才用传入值补位),两边先触发的一方决定结果:沙箱内超时会让任务以执行超时失败,外层超时则表现为工具调用超时。

产物路径必须位于 /workspace 内,单产物默认上限 10MB、产物总量默认上限 10MB(SANDBOX_ARTIFACT_MAX_FILE_SIZE_MB / SANDBOX_ARTIFACT_MAX_TOTAL_SIZE_MB)。边界明细见 工具与 Skills 的沙箱边界。

安全模型

  • 任务负载在全新的 Bubblewrap 用户+挂载命名空间内执行,绝不会运行在 Worker 自身的命名空间里。
  • 项目提供的部署让 Sandbox Worker 以 root 运行,并在运行时默认 cap 集上叠加 CAP_SYS_ADMIN:镜像的非 root 用户 effective capabilities 恒为空,而特权 Worker 即使在宿主禁止非特权用户命名空间时也能创建用户命名空间。Worker 保持 allowPrivilegeEscalation=false,且仅对 sandbox-worker 使用 seccomp=unconfined。
  • 禁止 seccomp=unconfined 的集群需要提供允许 namespace/mount 系统调用的 Localhost seccomp profile。
  • 任务命名空间内只有当前工作空间及其临时目录可写;依赖缓存和必要运行时目录只读挂载。
  • 子进程只接收过滤后的环境变量,而不是 Worker 的完整进程环境。
  • 会话过期后自动清理工作目录。

宿主内核要求

Bubblewrap 通过 unshare(CLONE_NEWUSER) 创建新的用户命名空间。项目提供的部署(Docker Compose、Helm、Kubernetes)让 Worker 以 root + CAP_SYS_ADMIN 运行,用户命名空间创建走特权路径,即使宿主限制非特权用户命名空间也能正常工作——无需修改宿主 sysctl。

自定义部署若保持 Worker 非 root,则依赖宿主内核允许非特权用户命名空间,否则所有沙箱任务都会失败,报错为:

bwrap: No permissions to create new namespace, likely because the kernel does not allow non-privileged user namespaces.

部分常见宿主发行版默认限制该能力,且 seccomp=unconfined 无法解决——限制发生在容器 seccomp profile 更底层的宿主内核:

发行版限制修复
Ubuntu 23.10+AppArmor 禁止非特权进程创建用户命名空间(kernel.apparmor_restrict_unprivileged_userns=1)sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
Debian / 旧内核用户命名空间克隆被禁用(kernel.unprivileged_userns_clone=0)sysctl -w kernel.unprivileged_userns_clone=1

检查当前状态并验证用户命名空间确实可创建:

sysctl kernel.apparmor_restrict_unprivileged_userns kernel.unprivileged_userns_clone 2>/dev/null
unshare -U true && echo "user namespaces OK"

使修改持久化:

echo 'kernel.apparmor_restrict_unprivileged_userns=0' > /etc/sysctl.d/99-clouisle-userns.conf
sysctl --system

注意事项:

  • sysctl 是宿主/节点级设置。在 Kubernetes 中无法按 Pod 设置:需要对每个节点生效(自管节点写入 /etc/sysctl.d/,托管集群使用自定义节点镜像或等效的节点初始化配置)。
  • 允许非特权用户命名空间是 rootless 容器(Bubblewrap、Flatpak、Podman)的标准前提。

加固:收敛 Worker 的 CAP_SYS_ADMIN

沙箱任务运行在全新的 Bubblewrap 用户+挂载命名空间内,无法直接触及 Worker 容器的 capabilities。但如果任务成功逃出 Bubblewrap,落点就是容器内 root + CAP_SYS_ADMIN。默认 Docker daemon 下容器与宿主共享初始用户命名空间,该 capability 是宿主 userns 级别的,经典逃逸链(cgroup release_agent、重挂 /proc 写入 kernel.core_pattern、sysctl 写入)在原理上可达。

  • Docker Compose:启用 daemon 用户命名空间重映射(/etc/docker/daemon.json 中 "userns-remap": "default"),让每个容器进入嵌套用户命名空间——CAP_SYS_ADMIN 只作用于容器自身的 userns,宿主逃逸链全部失效。沙箱 Worker 仍能在重映射后的命名空间内特权创建自己的 Bubblewrap userns,沙箱功能不受影响。启用后需重设既有命名卷属主,且 daemon 上所有容器都会被重映射。
  • Kubernetes:不适用 daemon 级重映射;依赖 NetworkPolicy 限制沙箱出网、及时升级 Bubblewrap 并关注其 CVE,或在集群提供节点级用户命名空间支持时使用该能力。

运行时开关与降级行为

沙箱链路上有四个关键开关,默认配置更偏向「代码总能跑起来」而不是「隔离一定生效」,生产部署需要逐项确认:

环境变量默认值作用
SANDBOX_RUNTIME_ENABLEDtrue设为 false 时完全不走沙箱网关,代码直接在 API/Worker 进程内执行(无任何隔离),仅供本地调试
SANDBOX_LEGACY_FALLBACK_ENABLEDtrue沙箱任务失败或网关抛异常时,回退到进程内的 legacy runner
SANDBOX_FILESYSTEM_ISOLATION_ENABLED代码 false;镜像/Compose/Helm/K8s true把可执行载荷包进 Bubblewrap 挂载命名空间
SANDBOX_FILESYSTEM_ISOLATION_BINARY代码 bwrap;部署 /usr/bin/bwrapBubblewrap 可执行文件名或绝对路径

为什么 SANDBOX_LEGACY_FALLBACK_ENABLED 值得单独关注:execute_code 这条调用路径在沙箱网关返回失败或抛出异常时,会静默改用进程内 legacy runner 重新执行,只在服务端日志留一条 warning。也就是说,沙箱 Worker 宕机、bwrap 缺失、队列无人消费这些情况,默认结果不是「代码被拒绝执行」,而是「代码退回到没有隔离的进程内执行」——隔离保护在无声中消失,而调用方拿到的是一个「成功」的结果。

要 fail-closed,把它设为 false:沙箱失败会直接把失败结果返回给调用方,不再降级。安全敏感的部署应当关闭该开关,并对沙箱任务失败率设告警。

反方向的坑同样要提防:文件系统隔离已启用时,找不到 bwrap、工作空间根目录不存在、或任务 cwd 逃出工作空间,启动器会抛 SandboxIsolationError 让任务失败——启动器自身不会退化成未隔离执行。但上层 SANDBOX_LEGACY_FALLBACK_ENABLED=true 仍会捕获这次失败并降级,所以只配置隔离开关、不关闭 fallback,并不能保证隔离一定生效。

相关配置

沙箱环境变量集中在 环境变量参考。启用文件系统隔离需要 SANDBOX_FILESYSTEM_ISOLATION_ENABLED=true 且 SANDBOX_FILESYSTEM_ISOLATION_BINARY 指向 Bubblewrap 可执行文件(Compose/Helm 默认 /usr/bin/bwrap)。Docker Compose 和 Helm 中的 sandbox-worker 默认使用 root + CAP_SYS_ADMIN + seccomp=unconfined 的安全配置;部署时若出现 bwrap 用户命名空间错误,见 故障排查。


参见:

这篇文章对你有帮助吗?

本页目录