NVIDIA对Agent安全问题交出的答卷:OpenShell深度架构分析

admin 2026-03-30 00:15:41 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: NVIDIAOpenShell是面向AIAgent的开源安全运行时,采用四层内核级沙箱(网络命名空间、Landlock、seccomp、权限降级)与声明式策略引擎,实现L7HTTP方法+路径级的细粒度访问控制,解决Agent获得系统权限后的安全风险。其治理-first的设计理念适合企业安全团队进行合规审计与精细控制,是AI安全领域的纵深防御实践。 综合评分: 75 文章分类: AI安全,安全建设,安全工具


cover_image

NVIDIA 对 Agent 安全问题交出的答卷:OpenShell 深度架构分析

原创

yzddMr6 yzddMr6

熵减矩阵

2026年3月26日 09:08 浙江

#

8 万行 Rust 代码,四层内核级沙箱,一套声明式策略引擎——OpenShell 是 NVIDIA 对 AI Agent 安全问题交出的答卷。这篇文章从架构深处拆解它的设计哲学、技术取舍,以及它给整个行业的启示。


1. 当 AI Agent 获得系统权限

OpenClaw 在短短几个月内席卷了开发者社区。它能帮你写代码、调试、部署,甚至自主完成整个开发任务。为了做到这些,它需要读写文件、执行 shell 命令、访问网络——换句话说,它需要你机器上几乎所有的权限。

这正是问题所在。一个拥有 shell 权限的 AI agent,同时也能读取你的 .env 文件里的数据库密码和云服务密钥,能 curl 任意 URL 把代码外泄到任何地方,能修改系统配置。而一旦 agent 遭遇 prompt injection 攻击——攻击者精心构造的恶意指令被注入到 agent 的上下文中——攻击者就继承了你给 agent 的全部权限。你的凭证、你的代码、你的基础设施,全部暴露在攻击面之下。

企业面对的困境是:禁用 AI agent 意味着放弃生产力优势,放开使用又意味着不可控的安全风险。目前没有好的中间方案。

这不是假设——Meta 禁止了 OpenClaw 在公司机器上运行,Microsoft 发布了专门的安全指南,CISA 在漏洞通报中引用了 OpenClaw。当 AI agent 从”聊天机器人”进化为”自主执行者”,安全模型必须跟着变。

现有方案各有不足:

  • • Docker 容器:网络隔离只能做到 IP+端口级别,无法区分 GET /repos 和 POST /repos/issues
  • • 云沙箱(E2B、Daytona、Modal):需要云服务,有成本和延迟,策略粒度不够
  • • 应用层约束(在 agent prompt 中说”不要访问敏感文件”):可被绕过,不可信

OpenShell 的核心主张很简单:安全控制必须在 agent 进程外部,由操作系统强制执行,agent 无法绕过。 这就是”浏览器标签页模型”——浏览器的沙箱不是靠网页 JavaScript 自我约束实现的,而是靠操作系统进程隔离。OpenShell 把同样的思路应用到 AI agent 上。


2. 项目全景与竞品定位

2.1 OpenShell 是什么

一句话:OpenShell 是一个开源的 AI agent 安全运行时,通过内核级沙箱 + 声明式 YAML 策略 + 推理隐私路由,让 Claude Code、Codex、OpenClaw 等 agent 在受控环境中运行。

项目由 NVIDIA 在 GTC 2026 发布,Apache 2.0 许可,作为 NVIDIA Agent Toolkit 的核心组件。目前处于 alpha 阶段(单用户模式),约 8 万行有效代码(Rust 为主 + Python SDK),10 个 Rust crate 组成 workspace。

2.2 架构鸟瞰

OpenShell 架构鸟瞰

四个核心组件各司其职:

| 组件 | 职责 | 关键特性 | | — | — | — | | Gateway | 控制平面:沙箱生命周期、策略分发、SSH 隧道 | 单端口复用 gRPC/HTTP/SSH,不执行策略 | | Sandbox | 数据平面:隔离执行、策略强制、流量拦截 | Landlock + seccomp + netns + OPA | | Policy Engine | 策略评估:L4 连接控制 + L7 请求检查 | 嵌入式 regorus(纯 Rust OPA),热更新 | | Privacy Router | 推理路由:凭证注入/剥离、模型覆写 | sandbox 内直连后端,不经 Gateway |

2.3 竞品光谱:从轻量到重量

AI agent 沙箱市场在 2025-2026 年快速分化。从轻到重,可以画出这样一条光谱:

AI Agent 沙箱竞品光谱:从轻量到重量

三个维度的对比:

安全模型

| 方案 | 隔离机制 | 网络策略粒度 | 凭证保护 | | — | — | — | — | | Membrane | Docker + eBPF | DNS 主机名白名单 | 无 | | Docker Sandboxes | Firecracker microVM | Allow/deny 列表 | 环境变量注入 | | E2B | Firecracker microVM | VM 级网络隔离 | SDK 管理 | | Modal | gVisor 系统调用拦截 | deny-by-default 出站 | 平台管理 | | OpenShell | Landlock+seccomp+netns | L7 HTTP 方法+路径级 | 占位符替换,agent 不持有真实密钥 |

OpenShell 是唯一能做到”允许 GET /repos/** 但拒绝 POST /repos/*/issues“的方案。这个粒度对企业合规至关重要——安全团队需要精确控制 agent 能做什么,而不是简单的”能不能访问 GitHub”。

开发者体验

| 方案 | 安装复杂度 | 首次启动时间 | 平台支持 | | — | — | — | — | | Membrane | 一条命令 | 秒级 | Linux | | Docker Sandboxes | Docker Desktop | ~125ms | 全平台 | | E2B | API key + SDK | ~150ms | 云端 | | OpenShell | install.sh + Docker | 分钟级(首次需部署 K3s) | Linux(alpha) |

OpenShell 的启动时间是最大的体验短板。首次 openshell sandbox create 需要拉取 K3s 镜像、部署集群、生成 mTLS 证书,整个过程可能需要 2-5 分钟。后续创建沙箱快得多(K8s Pod 调度),但首次体验远不如 docker run 那样即时。

架构哲学

| 方案 | 核心理念 | 适用场景 | | — | — | — | | E2B/Daytona | SDK-first:通过 API 管理沙箱生命周期 | AI 应用开发者,需要编程式控制 | | Modal | 平台-first:沙箱是 AI 基础设施的一部分 | ML 团队,需要 GPU + 推理 + 沙箱一体化 | | Docker Sandboxes | 简单-first:一个 Docker 容器搞定 | 个人开发者,需要最低门槛 | | OpenShell | 治理-first:策略即代码,可审计可版本控制 | 企业安全团队,需要合规审计和精细控制 |

NVIDIA 选择了”最重”的方案,这不是技术洁癖,而是目标用户决定的。OpenShell 的目标不是让个人开发者快速上手,而是让企业安全团队能够:用 YAML 定义策略 → git 管理 → code review → CI 验证 → 审计追踪。这条路径上的每一步都需要 OpenShell 提供的基础设施。


3. 纵深防御:四层安全隔离架构

OpenShell 的安全模型不依赖单一机制。它将四种独立的 Linux 内核安全原语组合成纵深防御体系——每一层都独立生效,即使某一层被绕过,其余层仍然提供保护。

四层纵深防御架构

3.1 网络命名空间 + veth pair:不可绕过的网络隔离

这是整个安全模型的基石。sandbox supervisor 在 fork agent 进程之前,创建一个独立的 Linux 网络命名空间,通过 veth pair 连接到主机命名空间:

主机命名空间                        Sandbox 命名空间
─────────────                      ─────────────
veth-h-{uuid}                      veth-s-{uuid}
10.200.0.1/24  ◄── veth pair ──►  10.200.0.2/24
     │                                  │
     ▼                                  ▼
代理监听器                          Agent 进程
(OPA 策略评估)                     (默认路由 → 10.200.0.1)
     │
     ▼
外部网络 (经策略过滤)

agent 进程在 pre_exec 中通过 setns(fd, CLONE_NEWNET) 进入 sandbox 命名空间——这是不可逆操作。进入后,agent 看到的网络栈中只有一个 veth 接口,默认路由指向代理地址。它根本看不到主机的网络接口。

为什么选择 veth pair 而非 iptables? iptables 方案中进程仍然共享主机网络栈,一个 CAP_NET_ADMIN 就能删除规则。veth pair 创建了物理上隔离的网络拓扑——即使 agent 获得 root 权限,它也无法直接访问主机网络。

绕过检测:supervisor 还在 sandbox 命名空间内安装 iptables OUTPUT 链规则,对任何绕过代理的直接出站流量记录 LOG 并 REJECT。后台 bypass_monitor 任务读取 /dev/kmsg 检测这些日志事件,提供快速失败的用户体验和诊断信息。

crates/openshell-sandbox/src/sandbox/linux/netns.rs:41-170

3.2 Landlock:文件系统访问白名单

Landlock 是 Linux 5.13+ 引入的非特权沙箱机制。OpenShell 用它实现文件系统访问控制——只有策略中明确列出的路径才能被访问,其余一律拒绝。

策略示例:

filesystem_policy:
  read_only: [/usr, /lib, /proc, /dev/urandom, /etc]
  read_write: [/sandbox, /tmp, /dev/null]

实现上,supervisor 为每个路径创建 PathBeneath 规则:只读路径获得 AccessFs::from_read 权限,读写路径获得 AccessFs::from_all 权限。调用 restrict_self() 后,规则对当前进程及所有后代生效,不可撤销。

执行顺序的微妙之处:Landlock 在 pre_exec 中的执行顺序是先 drop_privileges(切换到 sandbox 用户),再 sandbox::apply(应用 Landlock + seccomp)。这个顺序至关重要——drop_privileges 需要读取 /etc/passwd 和 /etc/group,而这些文件可能被 Landlock 规则限制。

兼容性策略:默认 best_effort 模式在内核不支持 Landlock 时静默降级(务实选择,降低采用门槛),生产环境应使用 hard_requirement 模式。

crates/openshell-sandbox/src/sandbox/linux/landlock.rs:1-89

3.3 seccomp BPF:系统调用级过滤

seccomp 是最后一道防线。它在系统调用层面阻止进程创建特定类型的网络连接:

| 网络模式 | 允许的 socket 类型 | 阻止的 socket 类型 | | — | — | — | | Proxy(默认) | AF_INET, AF_INET6, AF_UNIX | AF_PACKET, AF_NETLINK, AF_BLUETOOTH, AF_VSOCK | | Block | AF_UNIX | 以上全部 + AF_INET, AF_INET6 |

Proxy 模式允许 AF_INET/AF_INET6 看起来像安全漏洞,但实际上不是——agent 运行在网络命名空间中,它创建的 TCP socket 只能到达 veth 对端(即代理)。seccomp 在这里的作用是纵深防御:阻止 AF_PACKET(原始数据包嗅探)和 AF_NETLINK(网络配置修改)等危险操作。

应用 seccomp 前必须先设置 PR_SET_NO_NEW_PRIVS,确保子进程不能通过 setuid 二进制文件绕过过滤器。

crates/openshell-sandbox/src/sandbox/linux/seccomp.rs:1-77

3.4 权限降级:六步验证的 drop_privileges

sandbox 进程以 root 启动(需要创建网络命名空间),但在 exec agent 之前必须放弃所有特权。drop_privileges 实现了六步验证序列:

  1. 1. initgroups() — 设置补充组
  2. 2. setgid() — 切换组 ID
  3. 3. 验证getegid() 匹配目标 GID
  4. 4. setuid() — 切换用户 ID
  5. 5. 验证geteuid() 匹配目标 UID
  6. 6. 验证setuid(0) 失败 — 确认无法重新获得 root

第 6 步是 CERT POS37-C 的硬化措施——某些内核配置下 setuid 可能不会完全放弃 saved-set-user-ID。显式验证确保这种边界情况被捕获。

crates/openshell-sandbox/src/process.rs:352-459

3.5 四层如何协同:一个请求的完整路径

当 sandbox 内的 Claude Code 执行 curl https://api.github.com/repos/octocat/hello-world 时:

  1. 1. seccomp 允许 socket(AF_INET, ...) — Proxy 模式下 TCP 被放行
  2. 2. 网络命名空间 将连接路由到 10.200.0.1:3128(代理地址)
  3. 3. curl 发送 CONNECT api.github.com:443
  4. 4. 代理 从 /proc 识别调用进程(curl, PID 42),构建 OPA 输入
  5. 5. OPA 引擎 评估 data.openshell.sandbox.network_action
  • • 端点匹配:api.github.com:443 在策略的 github_api 规则中 ✓
  • • 二进制匹配:/usr/bin/curl 在规则的 binaries 列表中 ✓
  • • 返回 "allow",匹配策略名 github_api
  1. 6. SSRF 防护 DNS 解析 api.github.com,验证 IP 不是内部地址 ✓
  2. 7. 代理返回 200 Connection Established,建立 TCP 隧道
  3. 8. TLS 自动检测 peek 前 8 字节,检测到 TLS ClientHello
  4. 9. MITM 终止 使用临时 CA 签发 api.github.com 证书,终止 TLS
  5. 10. L7 检查 解析 HTTP 请求:GET /repos/octocat/hello-world
  • • OPA 评估 data.openshell.sandbox.allow_request
  • • 策略配置了 access: read-only(展开为 GET/HEAD/OPTIONS)
  • • GET 匹配 ✓,路径 /repos/** 匹配 ✓
    1. 代理重新加密并转发到真实的 api.github.com
  1. 12. Landlock 确保 curl 只能写入 /sandbox 和 /tmp

如果同一个 curl 尝试 POST /repos/octocat/hello-world/issues,步骤 1-9 相同,但步骤 10 会失败——POST 不在 read-only preset 中,代理返回 403 {"error":"policy_denied"}


4. 策略引擎:从 YAML 到内核级执行

4.1 嵌入式 OPA:为什么用 regorus 而不是外部 OPA daemon

OpenShell 选择了 regorus——一个纯 Rust 的 Rego 评估器,而非官方的 Go OPA 运行时。这个选择有三个原因:

  1. 1. 无 CGO 依赖:官方 OPA 需要 Go 运行时,交叉编译困难。regorus 是纯 Rust,与项目其余部分统一工具链
  2. 2. 微秒级延迟:策略评估在每个 CONNECT 请求上执行,必须极快。regorus 通过 Mutex<regorus::Engine> 序列化访问,单次评估在微秒级完成
  3. 3. Arc 共享编译策略:通过 arc feature,clone 引擎只复制解释器状态,编译后的策略通过 Arc 共享。这让 L7 检查可以获得独立的引擎副本而不重新编译策略

引擎的 Rego 规则被编译进二进制(include_str!("../data/sandbox-policy.rego")),不依赖外部文件。策略数据(网络规则、文件系统配置等)通过 JSON 注入到 OPA 的 data 命名空间。

crates/openshell-sandbox/src/opa.rs:59-147

4.2 进程身份识别:谁在发起这个连接?

策略不仅控制”能访问哪个端点”,还控制”哪个二进制能访问”。代理在收到 CONNECT 请求时,需要识别发起连接的进程身份。

识别链通过 /proc 文件系统实现:

  1. 1. 从 TCP 连接的源端口查找 /proc/net/tcp,定位到 inode
  2. 2. 遍历 /proc/*/fd/ 找到持有该 inode 的进程
  3. 3. 读取 /proc/{pid}/exe 获取二进制路径
  4. 4. 沿 /proc/{pid}/status 的 PPid 字段向上遍历祖先链
  5. 5. 读取 /proc/{pid}/cmdline 获取命令行参数中的路径

OPA Rego 规则用四种策略匹配二进制身份:精确路径、祖先链匹配(如 claude 启动了 node,node 的祖先链中有 claude)、glob 模式(/usr/bin/*),以及一个重要的安全决策——cmdline_paths 被显式排除于 glob 匹配,因为 argv[0] 可以被 execve 轻易伪造。

BinaryIdentityCache 实现了 TOFU(Trust-On-First-Use)完整性检查:首次看到某个路径时计算 SHA256 哈希并缓存,后续访问时验证哈希一致。这防止了运行时替换二进制文件的攻击。

crates/openshell-sandbox/src/procfs.rsidentity.rsdata/sandbox-policy.rego:128-153

4.3 L7 MITM:TLS 自动检测与终止

当 OPA 允许一个 CONNECT 隧道后,代理需要决定是否进行 L7 检查。这取决于端点配置中是否有 protocol 字段。

TLS 处理采用自动检测模式:代理 peek 隧道的前几个字节,如果看到 TLS ClientHello(0x16 0x03),就执行 MITM 终止。具体流程:

  1. 1. sandbox 启动时生成临时 CA(rcgen),写入 /etc/openshell-tls/
  2. 2. agent 进程的环境变量 SSL_CERT_FILENODE_EXTRA_CA_CERTS 等指向包含系统 CA + sandbox CA 的 bundle
  3. 3. 代理使用 sandbox CA 为目标主机名签发临时叶子证书(CertCache 按主机名缓存)
  4. 4. 与 agent 建立 TLS 连接(使用临时证书),与上游建立独立的 TLS 连接(使用系统 CA)
  5. 5. 在两个 TLS 连接之间解密、检查、重新加密

tls: skip 配置可以跳过 TLS 检测,直接建立原始隧道——用于不需要 L7 检查的场景(如数据库连接)。

crates/openshell-sandbox/src/l7/tls.rsl7/relay.rsl7/rest.rs

4.4 策略热更新与 Last-Known-Good

策略不是一次性加载的。运行中的 sandbox 可以接收策略更新,无需重启:

策略热更新时序

关键设计:只有 network_policies(动态字段)可以热更新。filesystemlandlockprocess(静态字段)在 sandbox 创建时锁定,因为 Landlock 的 restrict_self() 和 setuid 都是不可逆操作。Gateway 的 UpdateSandboxPolicy RPC 会拒绝任何修改静态字段的请求。

LKG(Last-Known-Good)行为确保安全:reload_from_proto 先构建完整的新引擎,成功后才原子替换旧引擎。如果新策略有验证错误(如 L7 规则格式错误),旧引擎保持不变,sandbox 继续使用上一个有效策略。

crates/openshell-sandbox/src/lib.rs:1305-1423

4.5 Policy Advisor:从拒绝事件到策略建议

OpenShell 不只是拒绝不合规的请求——它还能自动生成策略建议。当 agent 的网络请求被拒绝时,sandbox 内的 DenialAggregator 收集拒绝事件,MechanisticMapper 将其转化为具体的策略修改建议,提交给 Gateway 供用户审批。

Policy Advisor 管道

这个管道的关键架构决策:所有分析都在 sandbox 侧运行。Gateway 只是一个薄的持久化+审批层,不生成建议也不调用 LLM。N 个 sandbox = N 个独立管道,天然水平扩展。

映射器生成的建议包含置信度评分(基于拒绝次数、端口识别度)和安全注释(私有 IP 警告、数据库端口警告)。用户可以在 TUI 中逐条审批([a] 批准 / [x] 拒绝),或批量审批。审批后的规则自动合并到活跃策略,sandbox 在下次轮询时热加载。

crates/openshell-sandbox/src/denial_aggregator.rsmechanistic_mapper.rs


5. K3s-in-Docker:一个大胆的架构赌注

5.1 为什么选择 Kubernetes

OpenShell 把整个 K3s 集群塞进一个 Docker 容器里。这是整个项目中最具争议的架构决策。

选择 K8s 的理由是实际的:

  • • 声明式沙箱管理:每个 sandbox 是一个 CRD(agents.x-k8s.io/v1alpha1/Sandbox),K8s 的 reconciliation loop 自动处理 Pod 调度、重启、清理
  • • 网络策略:Helm 管理的 NetworkPolicy 限制 sandbox Pod 的 SSH 入站只能来自 Gateway Pod,防止集群内横向移动
  • • 存储原语:Gateway 的 SQLite 数据库通过 PersistentVolumeClaim 持久化,Pod 重启不丢数据
  • • Helm 部署:所有组件通过 Helm chart 声明式部署,版本升级是 chart 更新

替代方案是直接使用 Docker API(docker run 创建容器),代价是需要自己实现:容器调度、健康检查、自动重启、网络隔离策略、存储管理。这些都是 K8s 已经解决的问题。

5.2 K3s-in-Docker:降低用户门槛

K3s 是 Rancher 的轻量 K8s 发行版,单二进制文件,内置 containerd、flannel CNI、local-path-provisioner。OpenShell 把它打包进一个 Docker 容器,用户不需要安装 K8s:

用户机器
└── Docker
&nbsp; &nbsp; └── openshell-cluster 容器
&nbsp; &nbsp; &nbsp; &nbsp; └── K3s (v1.35.2-k3s1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├── Gateway StatefulSet
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├── Sandbox Pod 1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├── Sandbox Pod 2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; └── Agent Sandbox CRD Controller

cluster-entrypoint.sh 是集群启动的关键脚本(506 行 shell),它解决了 Docker-in-K3s 的一系列兼容性问题:

  • • DNS 代理:Docker 的内部 DNS(127.0.0.11)对 K3s Pod 不可达,脚本通过 iptables DNAT 将容器 eth0 IP 的 53 端口转发到 Docker DNS
  • • cgroup v1 兼容:K8s 1.35+ 默认拒绝 cgroup v1,脚本检测并添加 --kubelet-arg=fail-cgroupv1=false
  • • flannel 目录预创建:防止 kubelet 在 flannel 写入 subnet.env 之前尝试创建 Pod sandbox
  • • NSSH1 密钥生成openssl rand -hex 32 生成 SSH 握手密钥并注入 Helm values

5.3 代价与权衡

K3s-in-Docker 的代价是真实的:

  • • 启动时间:首次部署需要拉取 ~1GB 的集群镜像,K3s 启动需要 30-60 秒,整个流程 2-5 分钟
  • • 资源开销:K3s 本身消耗 ~200MB 内存,加上 etcd、CoreDNS、flannel 等组件
  • • 调试复杂度:问题可能出在 Docker 层、K3s 层、Helm 层、应用层的任何一个。项目为此提供了 openshell doctor 命令和 debug-openshell-cluster agent skill
  • • 平台限制:网络命名空间和 Landlock 需要 Linux 内核,macOS/Windows 只能通过远程 Linux 主机使用

如果重新设计:对于单用户场景,直接使用 Docker API + 自定义网络可能更合适——启动快、资源少、调试简单。但 OpenShell 的目标是多租户企业部署,K8s 的调度、网络策略、RBAC 在那个场景下是必需的。当前的 alpha 阶段用 K3s-in-Docker 作为”单用户模式的 K8s 体验”,是一个合理的过渡策略。

deploy/docker/cluster-entrypoint.shcrates/openshell-bootstrap/


6. Gateway 控制平面

Gateway 是 OpenShell 的”大脑”,但它有一个严格的自我约束:只协调,不执行。即使 Gateway 被完全攻破,已运行的 sandbox 仍然受内核级策略保护。

6.1 单端口协议复用

Gateway 只暴露一个端口(默认 8080),同时服务三种协议:

Gateway 单端口协议复用

这不是偷懒——在 K3s 集群内 NodePort 资源有限,在 Cloudflare Tunnel 场景下每个额外端口都意味着额外配置。MultiplexedService 按请求级别路由(不是连接级别),使用 hyper_util 的 serve_connection_with_upgrades() 支持 HTTP CONNECT 和 WebSocket 升级。

crates/openshell-server/src/multiplex.rs

6.2 持久化:万物皆对象的单表设计

Gateway 用一张 objects 表存储所有业务实体——Sandbox、Provider、SshSession、InferenceRoute、Settings 全部序列化为 protobuf blob。这个设计看起来粗暴,但对 Gateway 的数据模型恰到好处:实体类型多(6 种)但数量少(几十到几百),查询模式简单(按 ID/名称查找),不需要 JOIN。

protobuf 编码让 schema 演进无痛——新增字段不需要 ALTER TABLE。三个 trait(ObjectTypeObjectIdObjectName)提供类型安全的泛型 CRUD,编译器确保你不会把 Sandbox 当 Provider 存。

SQLite 用于本地开发(自动创建文件),Postgres 用于生产多副本部署。两个后端在连接时自动运行迁移。

crates/openshell-server/src/persistence/

6.3 SSH 隧道:HTTP CONNECT + NSSH1 握手

sandbox 运行在 K3s Pod 中,没有公网 IP。用户通过 SSH 访问 sandbox 的路径是:

CLI → ssh-proxy 子命令 → HTTP CONNECT /connect/ssh → Gateway → NSSH1 握手 → Sandbox SSH daemon

NSSH1 是 OpenShell 自定义的握手协议:NSSH1 <token> <timestamp> <nonce> <hmac>\n。HMAC-SHA256 签名证明连接来自授权的 Gateway,timestamp + nonce 防止重放攻击(sandbox 维护 nonce 缓存,60 秒过期清理)。

sandbox 的 SSH daemon 基于 russh,每次启动生成临时 Ed25519 主机密钥,接受任何 SSH 认证(因为 NSSH1 已经完成了认证)。shell 进程在 PTY 中启动,继承完整的 sandbox 策略(Landlock + seccomp + netns + 权限降级)。

crates/openshell-server/src/ssh_tunnel.rscrates/openshell-sandbox/src/ssh.rs


7. 推理路由与隐私保护

7.1 inference.local:透明代理设计

OpenShell 的推理路由解决了一个根本性的信任问题:agent 需要调用 LLM API,但它的凭证不应该泄露,敏感上下文也不应该发送到外部模型。

设计方案是一个虚拟主机 inference.local。agent 代码(如 Claude Code)被配置为将推理请求发送到 https://inference.local/v1/chat/completions,sandbox 代理拦截这个请求并路由到配置的后端。

推理路由时序

7.2 为什么推理在 Sandbox 内路由而不通过 Gateway

传统做法是让所有推理请求经过 Gateway 代理。OpenShell 选择了不同的路径——路由器运行在 sandbox 内部,直连后端。原因有二:

  1. 1. 延迟:LLM 流式响应对 time-to-first-token 敏感,额外的 Gateway 跳转增加延迟
  2. 2. 瓶颈:Gateway 成为所有推理流量的单点,N 个 sandbox 的推理请求都经过一个进程

Gateway 只负责控制平面:通过 GetInferenceBundle gRPC 将解析后的路由配置(含后端凭证)下发给 sandbox。sandbox 内的路由器直接与后端通信。

7.3 凭证注入/剥离的四层防护

路由器的 send_backend_request 实现了四层请求改写:

  1. 1. 认证注入:根据 provider 类型注入正确的认证头(OpenAI 用 Authorization: Bearer,Anthropic 用 x-api-key
  2. 2. Header 清洗:强制剥离 authorizationx-api-keyhost,防止 agent 凭证泄露
  3. 3. 默认 Header 注入:如 Anthropic 要求的 anthropic-version: 2023-06-01(尊重 agent 已发送的同名 Header)
  4. 4. Model 覆写:请求体中的 model 字段被强制替换为路由配置的值

更进一步,sandbox 的凭证管理采用占位符替换机制:agent 环境变量中的 API key 被替换为 openshell:resolve:env:ANTHROPIC_API_KEY 占位符。真实密钥只存在于 supervisor 进程内存中,代理在转发时替换。即使 agent 被完全攻陷,攻击者也无法从进程内存中提取真实密钥。

crates/openshell-router/src/backend.rs:85-158crates/openshell-sandbox/src/secrets.rs


8. CLI、运维与可观测性

8.1 一条命令从零到沙箱

OpenShell 的 CLI 设计目标是极致简化首次体验:

openshell sandbox create -- claude

这一条命令背后发生了什么:

  1. 1. CLI 尝试连接 Gateway → 失败(没有集群)
  2. 2. should_attempt_bootstrap() 判断是连接性错误 → 触发自动引导
  3. 3. Bootstrap 拉取 K3s-in-Docker 镜像,创建集群容器
  4. 4. 等待 K3s 就绪,部署 Helm chart
  5. 5. 生成 mTLS 证书,存储到 ~/.config/openshell/
  6. 6. detect_provider_from_command("claude") → 需要 Anthropic provider
  7. 7. 从 $ANTHROPIC_API_KEY 环境变量自动创建 provider
  8. 8. 发送 CreateSandbox gRPC,开启 WatchSandbox 流跟踪进度
  9. 9. Pod 就绪后,通过 SSH ProxyCommand 连接到 sandbox

should_attempt_bootstrap() 的判断逻辑很精细:它区分”Gateway 不存在”(连接拒绝、DNS 失败)和”Gateway 存在但配置错误”(证书验证失败)。前者触发自动引导,后者直接报错。

8.2 文件同步:tar-over-SSH

文件上传使用 tar-over-SSH 而非 rsync:CLI 将本地文件打包为 tar 流,通过 SSH stdin 管道传输,远端执行 tar xf - -C /sandbox 解压。

权衡:不需要 sandbox 内安装 rsync(减少依赖),流式传输不需要中间文件,但没有增量同步能力。对于 OpenShell 的场景(一次性推送项目文件到本地网络的 sandbox),全量传输的开销可以接受。

crates/openshell-cli/src/ssh.rs:454-525

8.3 TUI:k9s 风格的实时监控

openshell term 启动一个基于 ratatui 的终端仪表盘,提供 Gateway、sandbox、provider 的实时监控。2 秒轮询 gRPC + 流式日志推送实现准实时可观测性,无需 Prometheus/Grafana。

日志查看器支持 vim 风格导航(j/k/g/G)、visual selection 模式、源过滤(Gateway/Sandbox)、OSC 52 剪贴板集成。Draft 审批面板实现了”人在回路”的策略治理——AI agent 推荐网络规则,运维人员在 TUI 中审批。

8.4 OCSF:企业级审计日志

OpenShell 将所有安全事件映射到 OCSF v1.7.0 标准格式(AWS、Splunk、IBM 联合推动的企业安全事件标准)。8 个事件类覆盖网络活动、HTTP 请求、SSH 会话、进程执行、安全检测、策略变更等。

双格式输出:shorthand(人类可读,用于 TUI)和 JSONL(机器可读,可直接导入 Splunk/Sentinel)。通过 thread-local 桥接与 Rust tracing 生态集成,同一事件同时写入两种格式。


9. 总结

拆完 8 万行代码,回过头来看 OpenShell,它其实在回答一个很朴素的问题:当 AI agent 强大到需要 root 权限才能干活,我们怎么确保它只干该干的事?

NVIDIA 给出的答案有真正的技术洞察。L7 策略粒度是整个方案中最有价值的部分——在它之前,没有开源方案能区分”读 GitHub 仓库”和”用你的身份创建 issue”。

推理路由的 inference.local 虚拟主机 + 凭证占位符替换,让 agent 代码零修改就能获得隐私保护。策略即代码的完整闭环(YAML → git → OPA → OCSF 审计),几乎是开箱即用的企业合规基础设施。但 K3s-in-Docker 的架构选择让启动时间比竞品慢了一个数量级,Linux-only 的限制砍掉了大量 macOS 开发者,alpha 阶段的成熟度离生产部署还有距离。这些取舍背后的逻辑是清晰的——NVIDIA 选择了企业治理优先于开发者体验——只是这条路注定走得更慢。

但比 OpenShell 本身更值得关注的,是它指向的方向。

AI agent 的权限膨胀是不可逆的趋势。从纯文本对话到文件读写,从命令执行到全自主开发——每一代产品都在索要更多的系统权限。这不是产品经理的贪心,而是能力边界的自然延伸。问题不在于要不要给权限,而在于怎么给。

当前的 agent 安全方案呈现出明显的两极分化:一端是 Membrane 这样的轻量方案,eBPF 做 DNS 白名单,几乎零开销但策略粒度粗;另一端是 OpenShell 这样的重量方案,四层纵深防御但部署门槛高。市场真正缺少的是中间地带——一个既有 L7 级策略粒度,又能秒级启动的方案。而且今天的方案大多绑定特定运行时:OpenShell 绑定 Linux 内核原语,E2B 绑定 Firecracker,Modal 绑定自家平台。一个真正通用的 agent 安全层,应该像 TLS 之于网络通信——无论底层是什么,上层都能获得一致的安全保证。

OpenShell 的策略即代码指向了一个有意思的未来:每个项目的仓库里不只有 Dockerfile 和 CI.yml,还有一份 agent-policy.yaml,声明式地定义 AI agent 能访问哪些 API、能读写哪些文件、能使用哪个模型。安全策略像代码一样版本控制、code review、CI 验证,融入现有的 DevOps 工作流,而不是成为一个独立的安全孤岛。

当 AI 的能力足够强大,约束它的方式就不能再是”请你不要这样做”的 prompt,而必须是操作系统级别的、不可绕过的、可审计的硬性边界。

这个笼子还很粗糙,但方向已经明确。剩下的问题是:谁来把它做得更轻、更通用、更无感。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:熵减矩阵 yzddMr6 yzddMr6《NVIDIA 对 Agent 安全问题交出的答卷:OpenShell 深度架构分析》

评论:0   参与:  0