内容提要
文章剖析 OpenShell v0.1.5 的 supervisor 源码,指出其安全设计核心是一份“不信任清单”:不信任对端连接(单向 TLS 加 JWT 三重校验)、不信任进程自报身份(靠内核 /proc 账本倒查)、不信任加密流量(TLS 终结并严格解析 HTTP)、不信任沙箱内凭证(占位符内容指纹与三层 fail-closed)、不信任恢复者(SIGSTOP 冻结、仅原实例可解冻)。整体贯彻能验证绝不相信、用结构隔离、默认失败即关闭的原则。
延伸解读
单向 TLS 加 JWT:认证逻辑的刻意拆分
文章指出官方文档称 mTLS,但源码两侧均为 with_no_client_auth(),实为单向 TLS。双向信任靠 JWT 补齐:沙箱用网关公钥验证 supervisor 携带的 token,并核对 sandbox_id、runtime_generation、auth_epoch 三个字段。证书只保证通道不被窃听,指挥资格由 JWT 决定。这种拆分让认证逻辑不依赖证书链的完整性,也解释了为何 token 正确仍可能被拒——实例钉死要求必须是原 supervisor 进程。
内核账本倒查:身份归因为何不信任 argv[0]
supervisor 判断请求由哪个程序发起时,不采信进程自报,而是沿 /proc 的 socket inode、fd 持有者和 exe 链接倒查,三个数据源均由内核维护。源码注释明确将 argv[0] 排除在授权信号之外,因其可被 execve 轻易伪造。哈希采用 TOFU 模型,首次记录后比对,文件变动即报完整性违规。代价是祖先链继承放行了子进程全部能力,这是安全与实用的明确交易。
TLS 终结与解析器级防走私:L7 执法的前提
supervisor 在中间拆开 TLS,用临时 CA 为沙箱客户端按主机名签发叶子证书,检查明文后再向上游重新加密。文章强调真正关键的不是终结本身,而是对 HTTP 解析器也不信任:逐字节读头部、拒绝裸 LF、强制严格 UTF-8、校验 authority 与 Host 一致、拦截 Content-Length 与 Transfer-Encoding 组合走私。原因是 L7 策略语义建立在解析结果上,解析器被绕过一次,全部规则等于零。判定引擎中 deny 永远赢,没有规则顺序可绕。
冻结与同实例恢复:故障时的默认姿态
连接断开后 boundary 对全部已注册进程组发 SIGSTOP,并启动 30 秒恢复计时;workload 自行发 SIGCONT 会被拒绝,绕道 tgkill 也被 seccomp 拦截。解冻权只属于持有有效 token 且实例 ID 与首次 attach 记录一致的原 supervisor 进程。30 秒无人来则先 SIGCONT 再 SIGTERM,让进程有机会跑完清理逻辑。文章还提到 supervisor 用阻塞读 stdin 的专职线程检测父进程存活,EOF 即自杀,没有心跳或轮询。
Q&A
OpenShell 的 supervisor 为什么被描述为“不信任任何人的进程”?
因为 supervisor 的设计核心是一份“不信任清单”:它不信任对端连接(单向 TLS 加 JWT 三重校验)、不信任进程自报身份(靠内核 /proc 账本倒查)、不信任加密流量(TLS 终结并严格解析 HTTP)、不信任沙箱内凭证(占位符内容指纹与三层 fail-closed)、不信任恢复者(SIGSTOP 冻结、仅原实例可解冻)。每条不信任都有源码行号对应,整体贯彻“能验证绝不相信、用结构隔离、默认失败即关闭”的原则。
supervisor 如何验证沙箱连接的对端身份?
supervisor 使用单向 TLS(with_no_client_auth)验证沙箱服务端证书,但真正决定指挥权的是 JWT:沙箱用网关公钥验证 supervisor 携带的 JWT,并核对 sandbox_id、runtime_generation、auth_epoch 三个字段与自身档案一致。此外还有三把锁:代次不符的 token 拒绝、凭证序号只增不减防回放、实例钉死(首次 attach 记录 supervisor 实例 ID,其他实例接管会被 WrongSupervisorInstance 挡回)。
supervisor 如何确定发起网络请求的真实程序?
supervisor 不让沙箱自报身份,而是通过内核 /proc 账本倒查:先在 /proc/<入口进程>/net/tcp 按连接四元组找到 socket inode,再扫描所有进程的 /proc/*/fd 找到持有该 socket 的 owner PID,最后 readlink /proc/<pid>/exe 获取真身路径。计算哈希时直接读活 inode,防止磁盘文件替换;argv[0] 因可被 execve 伪造而被永久排除在身份信号之外。
supervisor 如何处理 HTTPS 流量以实施 L7 策略?
supervisor 在中间终结 TLS:启动时生成临时 CA 注入沙箱信任链,对沙箱客户端按主机名现场签发叶子证书,检查完明文后再用真实系统根证书向上游重新加密。对明文 HTTP 解析器也不信任,实施严格解析(防流水线走私、拒绝裸 LF、强制 UTF-8、校验 Host 头等),确保 L7 策略语义不被绕过。判定引擎使用嵌入式 Rego,deny 永远赢。
沙箱环境变量中的占位符(如 openshell:resolve:env:...)是什么?如何防止凭证泄露?
占位符是内容指纹:gateway 对 provider 身份、凭证键、过期时间、策略绑定整体做 SHA-256 取前 8 字节,不是版本号或随机数。注入发生在 supervisor 拿到明文后、转发给上游前,覆盖请求头、query、path、body、WebSocket 等位置。fail-closed 有三层:未解析的占位符绝不转发、改写后后置扫描确认无残留、日志中敏感信息用 [CREDENTIAL] 替身。凭证端点由 provider 声明绑定,网络放行但端点不符会返回 403 credential_endpoint_mismatch。
如果 supervisor 崩溃或断开,沙箱内的进程会怎样?
如果沙箱已进入 Connected 状态,连接断开后 boundary 会对全部已注册进程组发 SIGSTOP 冻结,并启动 30 秒恢复计时。冻结期间进程无法自救(SIGCONT 被拒、tgkill 被 seccomp 拦截)。只有持有有效 token 且实例 ID 与首次 attach 一致的原来那个 supervisor 才能解冻。30 秒内无人恢复,则先 SIGCONT 再 SIGTERM 让进程退出。若在 Confirm 前收到 SIGTERM,进程直接错误退出(教程中 gateway 重启后 workload 齐死的原因)。