OpenSandbox 项目学习
OpenSandbox 项目学习
项目地址:https://github.com/opensandbox-group/OpenSandbox
一、它是什么
一句话:OpenSandbox 是面向 AI 应用的通用沙箱平台(general-purpose sandbox platform),提供多语言 SDK、统一的沙箱协议,以及 Docker / Kubernetes 两套运行时,覆盖 Coding Agent、GUI Agent、Agent 评测、AI 代码执行、RL 训练等场景。
为什么需要它?如今 Agent(尤其 Coding Agent、代码解释器、浏览器自动化、RL 训练)需要在隔离环境里跑不可信代码、装第三方依赖、访问网络、长出进程,甚至要批量起几万实例。直接在宿主机上跑既不安全也不可规模化。OpenSandbox 把「隔离 + 可编程 + 可调度」这三件事标准化了:
- 隔离:容器级隔离,并可选 gVisor / Kata / Firecracker microVM 强化;网络出入站可策略化。
- 可编程:统一的生命周期 API + 沙箱内执行 API(命令、文件、代码解释器、PTY),多语言 SDK + CLI + MCP。
- 可调度:本地 Docker 单机跑,也能上 K8s 用 BatchSandbox / Pool 做池化与高吞吐交付。
它已进入 CNCF Landscape、有 OpenSSF Best Practices 徽章,是阿里开源、社区化运营的项目。
二、整体架构:六个表面
OpenSandbox 的设计核心是分层 + 协议优先。官方把系统拆成六个「表面(surface)」,理解这六个面就能拿住全貌:
- Client surface — SDK(Python/JS/TS/Java/Kotlin/C#/.NET/Go)、
osbCLI、MCP server。 - Protocol surface —
specs/下的 OpenAPI 契约(生命周期、诊断、execd 执行、egress 策略),是整个系统的「真理之源」。 - Lifecycle control plane —
server/下的 FastAPI 服务器,负责鉴权、校验、编排、端点解析、诊断、持久化。 - Runtime backends — Docker(本地/单机)与 Kubernetes(通过 BatchSandbox 或
kubernetes-sigs/agent-sandbox工作负载提供方)。 - Sandbox data plane — 用户工作负载容器 + 注入的
execd守护进程 + 可选 Jupyter/code-interpreter + 卷 + 可选 egress sidecar。 - Network & security plane — 端点解析、server 代理、K8s ingress 网关路由、安全端点访问、egress 策略、资源限额、安全容器运行时。
这种切分是有意的:
SDK 和工具依赖公共契约;server 只负责生命周期编排;runtime provider 负责平台相关的资源创建;
execd/egress 负责沙箱网络与文件命名空间内的操作。
也就是经典的 control plane vs data plane + runtime-neutral API, runtime-specific execution。
三、协议面(specs/)
OpenSandbox 把 specs/ 当作公开契约的唯一来源(single source of truth),改 spec 等于改公开 API,要同步动 server / SDK / docs / CLI。主要四份:
-
sandbox-lifecycle.yml— 生命周期 API,base path/v1,由 server 提供。包含:- Sandboxes:从 image 或 snapshot 创建、list、get、delete、pause、resume、续期、解析端口端点。
- Snapshots:从 sandbox 创建持久快照、list、get、delete。
- 请求体特性丰富:
image/snapshotId启动源、entrypoint/环境变量/metadata/不透明extensions、resourceLimits(CPU/内存/GPU)、platform约束、volumes(host / pvc / ossfs)、networkPolicy(egress sidecar 配置)、secureAccess(K8s ingress 网关端点凭证)。
-
execd-api.yaml— 沙箱内执行 API,由execd暴露:/ping、/code/contexts、/code、/session、/command、后台命令日志、/files/*、/directories、/metrics、/metrics/watch。命令与代码执行用 SSE 流式输出,另有 PTY WebSocket(/pty)做长连接 shell。 -
egress-api.yaml— egress sidecar 直接暴露的策略 API:GET /policy、PATCH /policy,支持运行时热更新。 -
diagnostic-api.yml— 沙箱日志/事件的最佳努力诊断描述,另有面向运维和 AI 排障工作流的纯文本诊断路由。
协议优先的好处:多语言 SDK 可以从同一份 OpenAPI 生成普通请求/响应客户端,再在之上加手写适配层做流式、传输生命周期、错误映射、高层模型。spec、实现、SDK、文档、配置、CLI 行为需要保持对齐。
四、控制面(server/)
server/ 是 FastAPI 生命周期服务器。关键包:
main.py— app 启动、中间件、路由注册、运行时校验、renew-intent 启动。api/— 生命周期路由、代理路由、池路由、诊断路由、请求/响应 schema。services/— 生命周期服务接口 + Docker / K8s 实现。services/k8s/— K8s 工作负载提供方、端点解析、卷/egress 辅助、informer。repositories/— 持久化适配器(目前用于 server 管理的快照记录)。integrations/renew_intent/— 可选的「访问即续期」集成。middleware/— API-key 鉴权、request ID 中间件。
几个值得记的设计点:
-
运行时服务二选一:
[runtime].type决定docker→DockerSandboxService或kubernetes→KubernetesSandboxService,两者实现同一个SandboxService接口,于是 API 路由很薄,只委托给 service,平台细节藏在 service 边界后。 -
持久化:
[store]选 server 管理的元数据存储,默认 SQLite(~/.opensandbox/opensandbox.db)。快照元数据是第一个被持久化的 server 资源;未来的持久化记录复用同一个 repository 边界。 -
端点解析 + server 代理:生命周期端点 API 返回沙箱内某端口的可达地址,按运行时与配置可能是:
- Docker host/bridge 映射端点;
- K8s ingress 网关端点;
use_server_proxy=true时的 server 代理 URL(/sandboxes/{sandboxId}/proxy/{port}),支持 HTTP 和 WebSocket,并可挂钩 renew-on-access。
五、运行时后端
5.1 Docker 运行时
本地/单机后端,直接和 Docker daemon 对话,管容器、timer、label、volume、port、可选 sidecar、snapshot。核心职责:
- 拉取公共/私有镜像(支持 per-request registry 鉴权);
- 用 CPU/内存/GPU/platform/capability/AppArmor/seccomp/PID/安全运行时设置建容器;
- 把
execd二进制从[runtime].execd_image暂存进沙箱,装一个 bootstrap launcher,再启动用户 entrypoint; - 网络
host/bridge/自定义网络;非 host 模式下为 execd 与用户服务端点分配宿主端口; - 服务重启后恢复已有受管容器的过期 timer;
- 支持 host bind mount、Docker named volume(
pvc模型)、OSSFS 挂载; - 请求
networkPolicy时挂 egress sidecar; - 把沙箱 commit 成本地镜像做持久快照、并支持从快照镜像恢复。
Docker 的 pause/resume 直接用容器级 pause/resume。
5.2 Kubernetes 运行时
K8s 运行时把真正的负载创建委托给 kubernetes.workload_provider 选定的工作负载提供方:
batchsandbox(默认)— 背靠 OpenSandbox 自己的 controller 和BatchSandboxCRD;agent-sandbox— 背靠kubernetes-sigs/agent-sandbox。
K8s 服务端路径负责:K8s client 初始化 + 可选 informer;从 image 请求建负载(snapshotId 启动时解析到存储的可恢复镜像);BatchSandbox 与 agent-sandbox manifest 的模板合并;per-request image pull secrets;资源限额 + GPU → K8s extended resources 的翻译;platform 约束 + RuntimeClass 接入安全运行时;卷、egress sidecar、安全端点访问注解;端点解析;pause/resume 委托给 provider;K8s 资源的纯文本诊断。
5.3 BatchSandbox Controller
kubernetes/ 下的 controller 实现了 OpenSandbox 专属 CRD,做高吞吐 + 池化的沙箱交付:
BatchSandbox— 从一个 pod template 建一个或多个沙箱副本;Pool— 维护预热资源,实现快速分配;SandboxSnapshot— K8s pause/resume 用的内部 rootfs 快照记录。
BatchSandbox 同时支持模板建法和池化建法(extensions.poolRef),并可选任务编排(batch / RL 类负载)。
K8s pause/resume 的实现很有意思:对 BatchSandbox.spec.replicas=1,pause 把沙箱 rootfs commit 成 OCI 镜像并释放运行时资源;resume 把负载模板改写为使用该快照镜像、重建运行时,同时保留 sandbox ID。注意公共快照 API 目前只有 Docker 运行时实现;K8s pause/resume 走的是 controller 内部 SandboxSnapshot 流程,是一套独立的运行时关注点。
六、沙箱数据面
6.1 execd
components/execd/ 是用 Gin 写的 Go 守护进程,跑在沙箱内部,暴露执行 API。职责:
- shell 命令执行(SSE 流式);
- 后台命令状态 + 增量日志拉取;
- 持久 bash session;
- PTY 交互式 session(WebSocket);
- 文件/目录操作;
- 基于 Jupyter 的代码上下文与代码执行;
- 本地 CPU/内存 metrics,可选 OpenTelemetry 导出;
- 可选
X-EXECD-ACCESS-TOKEN共享访问令牌强制。
execd 的注入方式:Docker 里由 server 把 execd 暂存进容器并装 bootstrap 脚本;K8s BatchSandbox 模板模式下,用一个 init container 从配置好的 execd_image 把 execd 和 bootstrap.sh 拷进一个 emptyDir volume,再挂给主沙箱容器。这是一种典型的「sidecar/initcontainer 注入控制进程」模式。
6.2 Code Interpreter 运行时
官方 code-interpreter 镜像在沙箱内启动 Jupyter,execd 通过 HTTP/WebSocket 和 Jupyter 通信,把 kernel message 翻译成 OpenSandbox 流式事件。镜像支持 Python/Java/Node.js/Go 运行时,以及 Python/Java/TS/JS/Go/Bash 的 Jupyter kernel,语言版本由 PYTHON_VERSION 等环境变量控制。Code Interpreter SDK 是可选高层客户端,底层执行 API 始终可用。
6.3 卷(Volumes)
生命周期 API 暴露运行时无关的卷模型:
host:绑定允许的宿主路径;pvc:平台管理的命名存储——Docker 映射成 Docker named volume,K8s 映射成 PVC;ossfs:通过 server/运行时集成挂载阿里云 OSS。
不同 provider 校验和物化方式不同,但 API 形状共享。
6.4 Egress Sidecar
components/egress/ 从沙箱网络命名空间强制出站网络策略:
- FQDN + 通配域名 allow/deny;
dns模式做 DNS 过滤;dns+nft模式在 DNS 之外用 nftables 强制已解析 IP 与 CIDR/IP 规则(环境支持时);- 运行时
/policy检查与 patch; - 可选 sidecar 鉴权、平台强制的 always-allow / always-deny 叠加、实验性透明 HTTPS MITM。
部署形态:Docker 把 egress 当独立容器起,主沙箱容器跑在 sidecar 网络命名空间里;K8s 把 egress sidecar 追加进 pod spec,并从主沙箱容器 drop 掉 NET_ADMIN,保证只有 sidecar 能改网络规则——这是个很干净的最小权限实践。
七、网络与访问
7.1 Ingress 网关
components/ingress/ 是 K8s 导向的 HTTP/WebSocket 反向代理,watch 沙箱资源并把流量路由到沙箱端口。路由模式:
- Header 模式:
OpenSandbox-Ingress-To: <sandbox-id>-<port>或 host 解析; - URI 模式:
/<sandbox-id>/<port>/<path>; - 通配 host 模式:经 server 端点格式化。
BatchSandbox 下 ingress 读 sandbox.opensandbox.io/endpoints 注解;agent-sandbox 下读 status.serviceFQDN。
7.2 Secure Access / Auto-Renew
secureAccess:当前仅支持经 ingress 网关暴露的 K8s 沙箱。开启后 server 给端点配凭证,端点响应里带回必需 header;网关 secure-access 签名密钥配好时还支持签名路由 token。- Auto-renew on access:可选 renew-intent 集成,观察到访问就延长沙箱 TTL,触发源可以是 server 代理请求或经 Redis 投递的 ingress 网关事件。单沙箱 opt-in 由
extensions["access.renew.extend.seconds"]控制。
八、核心流程串讲
把上面几节串成五条主线:
1. 沙箱创建(异步):
1 | Client/SDK/CLI/MCP |
客户端应轮询 GET /v1/sandboxes/{sandboxId} 或用 SDK 就绪辅助方法。
2. 命令/文件/代码执行:
1 | Client |
3. 服务暴露:
1 | Client |
4. Egress 策略:
1 | 创建请求带 networkPolicy |
5. Pause/Resume 与 Snapshot:
1 | Pause/Resume |
九、设计原则小结
读完源码与架构文档,OpenSandbox 反复强调几条原则,很值得做基础设施时借鉴:
- Protocol First — 公开行为从
specs/的 OpenAPI 契约出发,生成产物要重新生成而非手工补丁。 - Control plane vs Data plane — server 只编排与校验,平台相关 provisioning 进 runtime service,沙箱内操作进 execd/egress。
- Runtime-neutral API, Runtime-specific execution — 共享概念(resource limits、volumes、endpoints、network policy、metadata),Docker/K8s 各自物化但保持契约。
- Secure defaults with explicit escape hatches — API-key 鉴权、无鉴权模式启动 guardrail、资源限额、capability drop、可选安全运行时、egress 控制、端点 header、平台网络隔离;更宽松的模式留给本地开发或显式运营选择。
- Observable failures — 沙箱状态有
state/reason/message/transition time;execd 暴露 metrics,server 暴露诊断,ingress/egress/execd 有日志与 OTel,request ID 全链路传播便于排障。
十、典型用例
- Coding agents:在隔离沙箱里跑 Claude Code / Gemini CLI / Codex CLI / Qwen Code / Kimi CLI 等。
- AI code execution:执行模型生成代码,用命令/文件/code-interpreter API 拿流式反馈。
- 浏览器自动化:受控文件系统与网络行为下跑 Chrome / Playwright。
- 远程开发:经沙箱端点暴露 VS Code Web、桌面、VNC、开发服务器。
- RL 与评测:用 K8s BatchSandbox、Pool、任务编排做高吞吐沙箱交付(一个 trial 一个沙箱)。
- 企业级隔离:组合安全运行时 + ingress + egress + 端点访问 header + K8s 部署控制。
十一、本地快速上手
1 | # 安装并配置沙箱服务器 |
Python SDK 起一个 code interpreter 的最小例子:
1 | import asyncio |
十二、实战:云上 Agent 接入沙箱
上面是本地把 server 起起来玩的路径。更常见的真实需求是:我在云上已经部署好了 agent,想给它接一个沙箱/起一个沙箱。这一节专门讲这条路。
12.1 先理清调用关系
1 | 你的 agent (云上) |
关键认知:agent 不直连沙箱,而是连 OpenSandbox server(控制面);server 负责把沙箱建起来并告诉你 execd 的端点;之后 agent 再直接打 execd 做命令/文件/代码执行。所以「接一个沙箱」=「让 agent 学会调 OpenSandbox server 的 API」。
12.2 前置:先有一台 server
如果云上还没有 OpenSandbox server,最快的起法(单机 Docker):
1 | # 在你云上某台机器,装好 Docker 后 |
规模化、要池化/高吞吐就上 K8s 部署(kubernetes/ 下有 Helm chart,用 BatchSandbox + Pool 预热)。生产建议直接走 K8s。
云上必须确认三件事:
- server 的域名/端口对 agent 可达(同 VPC 或公网 + 鉴权);
- server 所在机器能访问 Docker daemon 或 K8s 集群;
- 配好
api_key,关掉无鉴权模式(server 有启动 guardrail)。
12.3 三种接入方式,按你的 agent 形态选
| 你的 agent 形态 | 推荐方式 | 说明 |
|---|---|---|
| 自研 Python/Java/JS/Go 后端服务 | SDK 直连 | 程序里 import 调用,最灵活,生产首选 |
| Claude Code / Cursor / 其它 MCP 客户端 | MCP server | 装一个 opensandbox-mcp,工具自动暴露给 agent |
| 运维脚本 / 临时排查 | osb CLI |
终端一行起沙箱 |
方式 A:SDK 直连(最常见)
装 SDK:pip install opensandbox(也有 Java/Kotlin、JS/TS、C#、Go 版本)。最小可用例子:
1 | import asyncio |
把它包进你 agent 的工具函数里,agent 就「接上沙箱」了。如果 agent 是同步栈,用 SandboxSync / ConnectionConfigSync,语义一致。
方式 B:MCP(给 Claude Code / Cursor 用)
1 | pip install opensandbox-mcp |
客户端配置(stdio):
1 | { |
之后 agent 自动多出「创建沙箱 / 跑命令 / 读写文件」几个工具,无需写代码。
方式 C:CLI(临时/运维)
1 | osb config set connection.domain sandbox.your-cloud.example |
12.4 云上生产的几个要点
- 一定要设
timeout:沙箱有 TTL,到期自动回收。长时间任务用renew(续期)或开启 auto-renew on access(extensions["access.renew.extend.seconds"]),有人访问就自动续命。 - 池化预热降延迟:如果 agent 高频起沙箱,用
Pool预热一批就绪沙箱,SDK 端用SandboxPoolSync直接 acquire,免掉冷启动。 - egress 策略:沙箱要访问外部 API/数据源时,用
networkPolicy挂 egress sidecar,只放行白名单 FQDN——不可信代码跑在沙箱里时这条尤其重要。 - 端点暴露:
get_endpoint(port)拿到地址;K8s ingress 网关模式下若开了secureAccess,响应会带回必需 header,agent 后续请求要带上。 - 用完
destroy():create-use-discard流程务必销毁,否则沙箱堆积吃资源;池化场景由池管理生命周期。 - 不可信代码隔离:跑模型生成代码/第三方依赖时,叠加 gVisor / Kata / Firecracker 安全运行时(RuntimeClass),别只用裸容器。
- 诊断:出问题用
GET /v1/sandboxes/{id}看state/reason/message,server 有纯文本诊断路由,execd 暴露/metrics,且全链路带 request id 便于排障。
12.5 一句话总结流程
部署/确认 server → agent 用 SDK(或 MCP/CLI)带 api_key 调 Sandbox.create → 拿 execd 端点跑命令/代码 → 用完 destroy()。
十三、读后感
OpenSandbox 的工程化水平相当成熟:把「沙箱」这种历史上散落在各种脚本里的东西,按协议 → 控制面 → 运行时 → 数据面 → 网络安全面严格分层,每层都用 OpenAPI 契约钉死边界,再让 Docker/K8s 各自实现。几个印象深刻之处:
- execd 注入模式:把控制进程暂存进沙箱、用 init container 拷进 emptyDir,是「在不可信环境里长出可控 API」的标准解法,比硬塞 agent 进容器干净得多。
- K8s pause/resume 用 rootfs 快照:commit 成 OCI 镜像 + 释放运行时 + 保留 sandbox ID,兼顾「省资源」和「ID 连续」,对 RL/评测的高频 pause/resume 很实用。
- egress 的最小权限:K8s 里从主容器 drop
NET_ADMIN、只让 sidecar 改规则,把「谁能改网络」收敛到一处。 - 协议优先 + 多语言 SDK 生成:避免「五份 SDK 五份行为」,是做开放平台该有的姿势。
后续可以顺着几条线深挖:specs/ 的 OpenAPI 全文 + SDK 生成流水线、BatchSandbox controller 的 reconcile 与池化调度算法、egress 的 dns+nft 双重过滤实现、以及 secure container runtime(gVisor/Kata/Firecracker)的具体接入点。
十四、底层技术栈与关键代码架构
读完架构文档还不够,真正想理解"沙箱底层用的是什么",得去翻代码。这一节给出按调用链梳理的实打实说明,均带文件位置。
14.1 底层技术栈
OpenSandbox 自己不发明容器/虚拟化技术,而是分层组合现成的内核与容器原语:
| 层 | 用什么 | 在哪 |
|---|---|---|
| 隔离(粗粒度) | Docker 容器 / Kubernetes Pod(+ 可选 RuntimeClass:gVisor、Kata、Firecracker microVM) | server/services/docker/、server/services/k8s/ |
| 隔离(细粒度,进程级) | bubblewrap (bwrap) + overlay + seccomp-bpf | components/execd/pkg/isolation/ |
| 控制面 | Python + FastAPI | server/opensandbox_server/ |
| 数据面(execd/ingress/egress) | Go 1.25 + Gin / gorilla-websocket / creack/pty / gopsutil / OTel | components/execd、components/ingress、components/egress |
| 调度面 | Go + kubebuilder 风格 controller + 自研 scheduler | kubernetes/ |
| 网络出入站 | nftables / iptables / 自建 DNS proxy / mitmproxy(透明 HTTPS MITM) | components/egress/ |
go.mod 里几个关键依赖很说明问题:elastic/go-seccomp-bpf(seccomp)、creack/pty(终端)、knative.dev/pkg(informer/controller 框架)、redis/go-redis(ingress 续期事件)、pelletier/go-toml(配置)。
14.2 关键认知:隔离是"两层"的
很多人以为"沙箱 = 一个 Docker 容器",OpenSandbox 不是。它有两层隔离:
第一层(容器/Pod 级):server 建一个 Docker 容器或 K8s Pod,可选叠加安全运行时。这一层隔离的是"这个工作负载 vs 宿主机/其他工作负载"。
第二层(进程级,execd 内部):这是最有料的部分。execd 跑在沙箱里,但它执行每条用户命令时,会再用 bwrap + seccomp 把这条命令包进一层隔离——独立的 mount namespace、tmpfs、overlay upper 层、seccomp-bpf 系统调用过滤。也就是说,沙箱里跑的命令本身还套着一层"OS 级沙箱"。
代码证据(components/execd/pkg/isolation/):
bwrap.go— 构造 bubblewrap 命令行 argv:bwrapNamespaceSegment(userns/mount)、bwrapTmpSegment、bwrapWorkspaceSegment、bwrapEnvSegment,并把 seccomp fd 通过--seccomp注入;seccomp_gen.go— 生成 seccomp-bpf filter;upper.go— overlay 的 upper 层(写时复制的工作区);probe.go— 启动时探测内核能力(isolation.Probe),决定能不能用 bwrap,不能用就降级(bwrap_stub.go报Available=false);main.go启动流程里:isolation.LoadConfig→isolation.Probe→ 拿到 isolator,后续runtime跑命令时Wrap(exec.Cmd)套上。
14.3 关键代码架构(按调用链)
控制面(server,FastAPI):
1 | client → FastAPI 路由 (api/) → services/factory.py + runtime_resolver.py |
main.py— app 启动、中间件、startup_guard(api_key 强制)、renew_intentconsumer;services/sandbox_service.py— 公共SandboxService接口,API 路由很薄,只委托;services/docker/—docker_service.py(创建)、container_ops.py、port_allocator.py、networking.py、volumes.py、ossfs_mixin.py、docker_diagnostics.py、snapshot_runtime.py;services/k8s/—kubernetes_service.py、batchsandbox_template.py(生成 BatchSandbox manifest)、workload_mapper.py、security_context.py(RuntimeClass/cap drop)、volume_helper.py;repositories/— SQLite 持久化快照元数据(默认~/.opensandbox/opensandbox.db);middleware/— api_key 鉴权 + request id。
Docker 运行时核心:docker_service.py:create_sandbox 做的事:拉镜像 → 建容器(挂 CPU/mem/GPU/capability/AppArmor/seccomp/PID/secure-runtime)→ 把 execd 二进制从 execd_image 暂存(staging)进容器 → 装 bootstrap.sh 启动器 → 配端口/host bind/named volume/OSSFS → 可选挂 egress sidecar → 启动用户 entrypoint。快照走 snapshot_runtime.py(把容器 commit 成镜像)。
Kubernetes 运行时 + 控制器:
1 | server kubernetes_service → 生成 BatchSandbox manifest → 提交到集群 |
CRD 状态机很清晰:Pending → Succeed,以及 Pausing ⇄ Paused ⇄ Resuming,失败转 Failed(条件类型有 Ready/Progressing/Paused/PauseFailed/ResumeFailed/PodFailed)。pause/resume 的 rootfs 快照逻辑挂在 controller 里。execd 注入走 init container 从 execd_image 拷进 emptyDir。
数据面 execd(Go):
1 | main.go → flag.InitFlags → isolation.LoadConfig/Probe → web(Gin) → controller/ |
runtime/command.go— 命令执行 + SSE 流式;command_status.go— 后台命令状态/增量日志;runtime/bash_session.go— 持久 bash 会话(exec.CommandContext+ export 解析保 cwd/env);runtime/pty_session.go—creack/pty+ WebSocket 长连接 shell;runtime/jupyter.go+pkg/jupyter/— code interpreter,把 kernel message 翻成流式事件;runtime/isolated_session.go— 命令级隔离会话(配合 bwrap);runtime/replay_buffer.go— 重放缓冲,断线重连能拿回历史输出。
网络面:
- ingress(
components/ingress/):Go 反代,pkg/proxy(HTTP/WS 转发)、pkg/sandbox(watch 沙箱资源拿端点)、pkg/signature(安全路由签名 token)、pkg/renewintent(消费 Redis 续期事件)。用knative.dev/pkg的 informer 框架做 watch。 - egress(
components/egress/):pkg/nftables(nft 规则)+pkg/dnsproxy(DNS 过滤)+pkg/iptables(降级方案)+pkg/mitmproxy(mitmproxy_transparent.go透明 HTTPS MITM)+pkg/credentialvault(凭证注入,真实 secret 不进工作负载)+pkg/policy(/policyGET/PATCH 热更新)。nft.go里的setupNft把静态策略写到 nft,再把 DNS 解析出的 IP 动态加进 allow set——这是 dns+nft 双重过滤的实现。
14.4 三个最值得看的实现点
- 进程级隔离层
components/execd/pkg/isolation/:bwrap 构造 mount namespace + overlay upper + seccomp-bpf 注入。沙箱里再套沙箱,这是它"通用 + 安全"的底座。 - K8s 调度器
kubernetes/internal/controller/{algorithm,strategy,poolassign,recycle,eviction}/+internal/scheduler/:池化预热、分配、回收、驱逐策略,这是高吞吐 RL/评测场景的核心,比单纯"起个 Pod"复杂得多。 - egress 的 dns+nft 动态联动
components/egress/nft.go+pkg/dnsproxy:DNS 过滤拿到解析 IP 后动态写入 nft allow set,既过滤域名又能处理 CDN 多 IP,加 mitmproxy 做透明 HTTPS 拦截 + credential vault 注入凭证——这是"可控出站"的完整工程化。
14.5 控制面 vs 数据面:执行边界
一个容易混淆的点:控制面 server 会根据请求创建 Pod,但它不在里面执行命令。执行走的是数据面(execd),agent 直连 execd,不再经过 server。两阶段、两条不同链路:
1 | ┌─────────────────────────────────────────────────────────────────┐ |
逐字澄清:
- 控制面根据请求创建 Pod —— 是。K8s 模式下
KubernetesSandboxService把 create 请求翻译成BatchSandboxmanifest 提交,controller 负责 reconcile 出真正的 Pod(services/k8s/kubernetes_service.py+batchsandbox_template.py)。Docker 模式下则是DockerSandboxService调 Docker daemon 建容器。 - 然后在里面执行对应的命令 —— 不是。Pod 起来后,server 的活就基本干完了(只剩端点解析/续期/诊断/快照这些生命周期管理)。执行命令是 agent 直接连 Pod 里的
execd,server 不参与、不做中转、不解析输出。
为什么这样设计:就是架构文档反复强调的 Control plane vs Data plane。server(控制面)只做编排与校验——鉴权、配额、资源限额翻译、工作负载创建、端点解析、pause/resume、快照元数据持久化,是"管理面",不碰业务数据流;execd(数据面)做沙箱内的一切实际操作——跑命令、读写文件、PTY、Jupyter 代码执行,它跑在 Pod 里,和用户工作负载共享网络/文件命名空间,所以能直接操作沙箱内部。好处是执行流量(可能很大,有流式输出、长连接 PTY)不经过控制面,server 不会被业务流量打爆;execd 失败只影响单个沙箱,不波及全局调度。
一个例外:server proxy。如果网络拓扑让 agent 摸不到 execd 直连地址,可以开 use_server_proxy=true,server 会用 /sandboxes/{sandboxId}/proxy/{port} 转发 HTTP/WebSocket 流量到 execd。但注意——这仍是转发,不是 server 自己执行命令;真正的执行还是在 Pod 里的 execd。这是逃生通道,不是默认路径。
落到代码位置:
| 行为 | 谁干 | 代码位置 |
|---|---|---|
| create → 建 Pod | 控制面 server | server/services/k8s/kubernetes_service.py → batchsandbox_template.py |
| BatchSandbox CRD → 真 Pod | K8s controller | kubernetes/internal/controller/ + apis/sandbox/v1alpha1/batchsandbox_types.go |
| 端点解析(告诉 agent execd 在哪) | 控制面 server | lifecycle endpoint API(/v1/sandboxes/{id}/endpoints/{port}) |
| 跑命令/文件/代码 | 数据面 execd | components/execd/pkg/web/controller/ + pkg/runtime/(command.go/bash_session.go/pty_session.go/jupyter.go) |
所以本质是:create 阶段过控制面建 Pod,execute 阶段 agent 直连 Pod 里的 execd——server 只管"把沙箱建起来并告诉你地址",不管"在沙箱里跑什么"。
一句话总结:底层 = Docker/K8s 容器(可叠安全运行时)+ execd 内部 bwrap/seccomp 进程级隔离;控制面 FastAPI,数据面/网络面/调度面全是 Go;关键代码集中在 server/services/docker|k8s、components/execd/pkg/{isolation,runtime,web}、kubernetes/internal/{controller,scheduler,task-executor}、components/{ingress,egress}。
十五、鉴权到底怎么做的(大白话版)
很多人第一反应是:这玩意跑在 K8s 上,鉴权肯定用 ServiceAccount token 吧?不是。 先把结论摆这儿:agent 访问沙箱全程不用 K8s 的 SA token,用的全是 OpenSandbox 自己生成的 api_key 和随机字符串;SA token 只在一个不沾边的地方出现——server 自己去调 K8s 接口时。
15.1 打个比方:进一个沙箱像进一套房子
把你要访问的沙箱想象成一套房子,要过四道门禁:
| 门禁 | 你到哪了 | 刷什么卡 | 谁发的卡 |
|---|---|---|---|
| 小区大门 | agent → server(控制面) | OPEN-SANDBOX-API-KEY |
运维配的 api_key |
| 楼栋门 | agent → ingress 网关 | OpenSandbox-Secure-Access + 签名 |
server 随机生成,塞进 K8s 注解 |
| 入户门 | agent → execd(数据面) | X-EXECD-ACCESS-TOKEN |
execd 启动时带的 token |
| 阳台门 | agent → egress sidecar | OpenSandbox-Egress-Auth |
server 随机生成 |
没有一道门用的是 K8s SA token。 全是"小区物业"自己印的卡。
15.2 SA token 到底在哪——只有一个地方
server/services/k8s/client.py 里:
1 | if self.config.kubeconfig_path: |
意思是:server 这个程序自己要去跟 K8s “说话”(建 Pod、查状态、建 PVC),它说话时得亮明自己是谁——这时才用 SA token(或 kubeconfig)。这就像物业经理进物业系统办业务要刷自己的工牌,跟业主(agent)能不能进房子毫无关系。agent 永远拿不到这张工牌,也不需要。
15.3 四道门禁逐个说
① 小区大门:api_key(OPEN-SANDBOX-API-KEY)
最外层。你在 ~/.sandbox.toml 里配的那个 api_key,agent 每次调 server 都得在 header 里带上。server 的 middleware/auth.py 负责验:单租户模式直接比对你配的那串;多租户模式拿 api_key 去 tenant_provider.lookup() 查你是哪个租户。没带或不匹配直接拒。还有个 startup_guard 守门:你要是不配 api_key 想裸跑,它不让你在公网上启动,防止"大门敞开"。
② 楼栋门:secureAccess(OpenSandbox-Secure-Access)
这一道只在你走 K8s ingress 网关模式、且开了 secureAccess 时才有。server 建沙箱时随手生成一个随机串(secrets.token_urlsafe(24),24 字节随机数编码成 url 安全字符串),写进这个沙箱的 K8s 注解里存着;你问"端点在哪"时,server 把这串塞进响应的 header 还给你。你后续请求网关就带着这个 header,网关(components/ingress/pkg/signature)对一下注解里的值,对得上才放行。如果还配了签名密钥,网关再验一个 HMAC 签名,防止这串被改过。
说句实在话:这串是明文存在 K8s 注解里的,网关只要有权限读注解就能看到。所以它的定位是"路由层挡一挡",不是什么高强度机密保护——真要防黑客级别的攻击,得靠后面那道入户门 + 网络隔离。
③ 入户门:execd token(X-EXECD-ACCESS-TOKEN)
这是真正到了 Pod 里、要操作沙箱(execd)时的门禁。execd 启动时通过 --access-token 参数或 EXECD_ACCESS_TOKEN 环境变量拿到一个 token;你调 /command、/files 这些接口必须在 header 里带 X-EXECD-ACCESS-TOKEN,execd 的 accessTokenMiddleware(pkg/web/router.go)拿你给的跟自己存的比对,对不上返回 401。
有个坑要注意:这串是 execd 启动参数传的,如果你不配(token 为空),execd 直接跳过校验——等于入户门没锁。本地开发可以这么干,生产环境务必配上。
④ 阳台门:egress auth(OpenSandbox-Egress-Auth)
挂了 egress sidecar、想运行时改出站策略(PATCH /policy)才用得上。同样是 server 随机生成一个 token,端点响应里带给你,你改策略时带上,sidecar 验。机制跟 secureAccess 一样,随机串 + header 比对。
15.4 为什么不用 SA token
不是不会用,是不该用:
- agent 多半不在 K8s 集群里,它压根拿不到 SA token;就算在,把 SA token 给 agent 等于把 K8s API 的钥匙交出去了——SA token 能干的事太多(看 secrets、建 Pod……),粒度太粗,给一个只想跑两条命令的 agent 太危险。
- 沙箱是短命的、一次性的,起起停停,每个都该有自己的临时钥匙。SA token 是长期的、namespace 级的"工牌",跟"用完就扔"的沙箱对不上号。
- server 当中间人发卡最合适:api_key 是你长期持有的"业主卡";secureAccess 和 egress 两把是 server 给每个沙箱临时随机发的"访客卡",沙箱一销毁就废;但 execd token 是运维在 execd 镜像/配置里静态配的,不随沙箱发放(详见 15.6)。
所以整套就是个自研的轻量凭据体系:一道长期 api_key 守大门,三类随机 token 守到 Pod 各个面,SA token 只服务 server 自己跟 K8s 打交道。
15.5 一句话总结
agent 进沙箱过四道门:api_key(进 server)、secureAccess token(进网关)、execd token(进数据面)、egress token(改出站策略)——其中 secureAccess/egress 是 server 现场随机发的,execd token 是运维静态配的(不配则不校验),没有一把是 K8s ServiceAccount token;SA token 只有 server 自己调 K8s API 时当"工牌"用,agent 看不见也用不着。代码都在 server/middleware/auth.py、server/services/endpoint_auth.py、server/services/k8s/{create_helpers,endpoint_resolver}.py、components/execd/pkg/web/router.go、components/ingress/pkg/signature。
15.6 secureAccess token vs execd token:别搞混这两把
secureAccess token 和 execd token 都是"门禁卡",但管的是两道不同的门、由不同的人发卡、在不同位置校验,而且是串联关系,不是替代关系。
对比表:
| 维度 | secureAccess token | execd token |
|---|---|---|
| header 名 | OpenSandbox-Secure-Access |
X-EXECD-ACCESS-TOKEN |
| 谁生成 | server 现场随机(secrets.token_urlsafe(24)) |
运维/execd 镜像静态配置(--access-token / EXECD_ACCESS_TOKEN) |
| 存哪 | K8s 注解 / Docker label | execd 进程内存 |
| 谁校验 | ingress 网关(components/ingress/pkg/signature) |
execd 自己(pkg/web/router.go:accessTokenMiddleware) |
| 校验位置 | agent → 网关 这一跳 | agent → execd 这一跳 |
| 适用场景 | 仅 K8s 网关模式 + 开了 secureAccess |
所有模式,只要 execd 配了 token |
| 粒度 | per-sandbox,每把不同 | 一个 execd 镜像/配置一把(可能多沙箱同值) |
| 防伪强度 | 随机串 + 可选 HMAC 签名(OSEP-0011) | 纯字符串比对,不配就跳过(等于没锁) |
| 作用对象 | 路由层:能不能走网关进这个沙箱 | 执行层:能不能调 execd 跑命令/读写文件 |
它俩是串联的,各管一段。K8s 网关模式 + secureAccess + execd 配了 token 的完整链路:
1 | agent 请求 |
一个请求要两道都过才能执行:secureAccess 管"你能不能从网关进来",execd token 管"你能不能调执行 API"。两把钥匙互不通用——网关不认 execd token,execd 也不认 secureAccess token。
几个容易混的点:
- Docker 模式下没有 secureAccess 这道门。secureAccess 只在 K8s ingress 网关模式有效(server 会校验
ingress.mode == gateway才允许开 secureAccess)。Docker 模式 agent 直连宿主端口到 execd,只过 execd token 这一道(若配了)。 - execd token 不配等于没锁。execd 中间件逻辑是
if token == "" → 跳过校验,空串直接放行。secureAccess 不一样——它要靠 server 显式生成并写进注解才存在,不生成就没有这道门的概念。 - 发卡方不同导致轮换难度不同。secureAccess 是 server per-sandbox 现场发的,沙箱一销毁就废,天然短期;execd token 是运维配的固定值,要轮换得改 execd 镜像/配置重启,多个沙箱可能共用同一把。
- 强度不同。secureAccess 可叠加 HMAC 签名(防 token 被改/重放);execd token 是裸字符串比对,没签名机制,主要防"随手乱调",不防"认真攻击"。
一句话:secureAccess token = 网关发的路由通行证(server 随机发、网关验、仅 K8s 网关模式);execd token = execd 自己的执行 API 钥匙(运维配、execd 验、所有模式)。一个管"进不进得来",一个管"能不能动手",串联在一起。
参考
- 项目:https://github.com/opensandbox-group/OpenSandbox
- 架构文档:
docs/architecture/index.md - 协议:
specs/sandbox-lifecycle.yml、specs/execd-api.yaml、specs/egress-api.yaml、specs/diagnostic-api.yml - ROADMAP:
ROADMAP.md







