让 Agent 调用模型却拿不到 API Key:OpenShell 实测、DSH 默认隔离与 SSH Key 的边界

九月 13, 2026 [网络与安全] #openshell #dsh #sandbox #landlock #SSH

背景

Agent 一旦有了 shell 工具,"密钥不进它上下文"就变成了一件很难保证的事:env、.env、~/.config/... 都是它伸手就能读的;prompt injection 还能诱导它把 key 发到外部。目标很明确:

Hermes / DSH / 任意 agent 能正常调用模型,但 agent 进程本身拿不到真实 API Key。

围绕这个问题最近做了一轮调研,并拿手头的环境做了实测(dut 上跑着 OpenShell + Hermes 沙箱,见把 Hermes 关进 OpenShell 沙箱)。顺手把 SSH key、DSH 的默认隔离、以及几个轻量替代方案一起理清。先给结论:

一、先分清三层机制

讨论"防泄漏"之前先分清防的是哪一段,否则很容易把三个东西混在一起:

层代表机制防住什么防不住什么
网络/应用层代理OpenShell supervisor proxy真 key 不进 agent 进程;出网端点白名单 + L7 审计agent 读宿主文件(靠别的层)
内核沙箱bwrap + Landlock + seccomp(DSH、容器)agent 读到宿主敏感文件 / 提权同用户下的进程逃逸、/proc/<pid>/environ
密码学SSH 公钥认证私钥在网络上被截获私钥被本地进程读走、签名能力被滥用

关于 OpenShell 的常见误解:MITM 只是它的实现手段(代理终结 TLS 才能替换凭证),不是目的。它真正做的是"凭证占位符 + 策略引擎 + 审计"三件套。

二、OpenShell 是怎么做到 agent 拿不到 key 的(dut 实测)

2.1 链路

CLI ──mTLS──▶ openshell-gateway(控制面,127.0.0.1:17670)
                  │  真 key 存在 gateway 的加密凭证库里
                  ▼
          sandbox 容器(rootless podman)
            └─ supervisor(PID 1)
                 ├─ 给 agent 进程注入占位符环境变量
                 ├─ 出网强制走 10.200.0.1:3128 正向代理
                 └─ 对 L7 端点终结 TLS,把占位符替换成真 key 再发上游

2.2 实测证据

agent 进程看到的环境变量长这样:

$ openshell sandbox exec -n hermes -- sh -c 'env | grep XIAOMI'
XIAOMI_TOKEN_PLAN_CN_API_KEY=openshell:resolve:env:v..._XIAOMI_TOKEN_PLAN_CN_API_KEY

真 key 只在 gateway 的加密库里,代理在发往 token-plan-cn.xiaomimimo.com:443 的请求上替换(临时 CA 的私钥只在 supervisor 内存里)。每次 allow/deny 都有 OCSF 日志:

$ openshell logs hermes --tail
HTTP:POST [INFO] ALLOWED POST http://token-plan-cn.xiaomimimo.com:443/v1/chat/completions [policy:hermes engine:l7]
NET:OPEN  [MED ] DENIED /usr/bin/curl -> example.com:443 [reason:endpoint not allowed by any policy]

2.3 policy 管什么

OpenShell 的 policy 分静态段(创建时锁定,改了要重建沙箱)和动态段(可热更):

字段类别作用
filesystem_policy.read_only / read_write静态Landlock 白名单,未列出的路径不可访问
landlock.compatibility静态best_effort / hard_requirement
process.run_as_user / run_as_group静态agent 降权身份;0/root 被 schema 拒绝
network_policies.<key>动态端点 × 二进制白名单;protocol: rest = L7 检查
request_body_credential_rewrite动态是否重写请求体里的占位符(默认 false,见 §3.4)

沙箱容器本身也不是"随便挂载"的:dut 上它只挂了 /sandbox 卷 + supervisor 二进制 + TLS 文件,宿主 home/ssh/pass 都不可见(这条很关键,后面 §2.5 会纠一个常见的误传)。

2.4 坑:占位符进入对话/请求体 = 会话永久失败

这是本次实测踩到的新坑,也是写这篇文章的起因。

Hermes 排查问题时用 terminal 工具跑了 env/读 .env,把 openshell:resolve:env:... 占位符留在了会话历史里。之后这个会话的每一次模型请求体里都带着占位符,而 OpenShell 代理按设计拒绝:

HTTP:POST [INFO] ALLOWED POST .../v1/chat/completions
NET:TRAFFIC [HIGH] DENIED ... [reason:POST request body credential traffic denied]
NET:FAIL ...

代理直接掐断连接,客户端报 APIConnectionError: Server disconnected without sending a response,Hermes 显示"model server not responding"——新会话(hermes -z)正常,只有被污染的会话全挂。

2.5 勘误:几个网上流传的 OpenShell 说法

调研时拿到的二手资料里有几处张冠李戴,照着配会踩空:

流传说法事实
"workspaceAccess: ro/rw 会把宿主工作目录挂进容器"这是 OpenClaw 的配置(~/.openclaw/openclaw.json),不是 NVIDIA OpenShell。OpenShell 沙箱只挂 /sandbox 卷和 supervisor/TLS 文件,不挂宿主工作目录
"tools.exec.host: gateway 时命令直接在宿主机执行、容器内 uid=0 就是宿主 root"同样是别家的说法。OpenShell 的 agent 进程在沙箱内降权运行,uid=0/root 被 policy schema 和 supervisor 双重拒绝
"未列出的端点会弹窗让你实时审批"OpenShell 是声明式 deny + OCSF 日志,没有交互审批
"审计日志不可篡改"OCSF 事件流可以看 allow/deny,但"不可篡改"是营销词,别当合规保证

三、SSH key:和 API key 是同一个问题吗?

不是。

3.1 SSH 私钥本来就不传输

SSH 公钥认证里,私钥永远留在本地,网络上传的是"用私钥对握手数据(含会话 ID)的签名"。中间人即使截获签名,也无法转发给真实服务器——因为它和客户端、服务器各自建了不同的连接,会话 ID 对不上,签名验证必然失败。所以 SSH key 的泄漏模型不是"传输被截",而是:私钥文件被读走,或者签名能力被滥用。

3.2 agent forwarding 是双刃剑

ssh -A / ForwardAgent yes 会把本地的 ssh-agent 通道借给远端:远端的进程可以请求你的 agent 签名。目标服务器一旦被攻破,攻击者就能用你的身份去连别的机器。除非明确需要跳板链,否则不要开。

3.3 在 OpenShell 里让 agent 用 SSH 但拿不到私钥?

现状要说清楚:OpenShell 没有现成的 ssh-agent 转发/挂载开关。沙箱容器的挂载由 driver 决定(我们这份配置只有 /sandbox 卷 + TLS 文件),你没法把宿主的 $SSH_AUTH_SOCK 塞进去;网络默认也只放行 80/443,22 端口需要显式加。所以网上那套"宿主机跑 ssh-agent + 沙箱里 ssh -o IdentityAgent=$SSH_AUTH_SOCK"在 OpenShell 里是配不出来的,除非改 driver / 做 BYOC 深度定制。

可行的路线,按推荐程度排:

  1. 短时 SSH 证书(step-ca / Vault / Teleport 之类的 SSH CA):给 agent 发分钟级/小时级证书。证书在沙箱里,但泄漏窗口极小、可撤销、可绑定主体。这是"让 agent 能 SSH"最稳的做法。
  2. 宿主侧受控执行器:私钥和 agent 留在宿主机,把"固定几条 ssh 命令"包成一个受控服务/脚本,通过 policy 放行的端口给沙箱调用。agent 只有"请求执行"的能力,没有 key。
  3. 专用受限 key 放沙箱:用远端 authorized_keys 的 command= / from= / no-pty / no-port-forwarding 约束死。实现最简单,但等于放弃"agent 看不到 key",只适合低价值目标。
  4. 不让沙箱做 SSH:这类操作留在宿主或 CI,agent 只要结果。

四、DSH(DeepSeek Harness)的默认隔离

这一节主要整理自公开资料,未在本机逐项实测;本机的 DSH 部署形态见caddy 反代 DSH Web。配置前建议以实际版本的文档/行为为准。

五、轻量方案对比

方案复杂度agent 拿得到真 key?适合场景
环境变量 / .env最低能(同用户进程可读 .env、/proc/<pid>/environ)只防误提交,不防 agent
独立 OS 用户跑 agent低跨用户读不到;同用户逃逸才行单机多用户、轻量加固
agent 的 shell 进容器中拿不到(key 留在宿主进程)个人日常,性价比高
本地代理网关(LiteLLM 等)中拿不到(agent 只有 fake token,真 key 在网关进程)多 key/多模型统一管理
OpenShell高拿不到,且带出网/文件/审计策略需要完整边界与可审计
DSH 内置沙箱低(对 DSH 而言)取决于 agent 形态;同用户边界仍在本来就用 DSH

一个容易忽略的点:"独立 OS 用户"防的是跨用户读取,不防同用户逃逸;"本地代理网关"防的是 key 泄漏,不防 agent 破坏宿主文件。选方案前先写清楚威胁模型。

六、照着配置的清单

  1. OpenShell / Hermes(已有环境)
    • 验证占位符:openshell sandbox exec -n hermes -- sh -c 'env | grep XIAOMI',应该只看到 openshell:resolve:env:...。
    • 纪律:agent 不允许 env | grep、cat .env(占位符进上下文会让会话永久失败,§2.4)。
    • cron 任务注意 provider 快照:全局换 provider 后,任务默认仍钉在创建时的快照上,需要 cron.model_provider 或 hermes cron edit --provider。
  2. 宿主 DSH
    • 确认默认沙箱生效(workspace-write);.credentials.yaml 权限 600;威胁模型包含同用户逃逸就换独立用户/容器。
  3. SSH
    • 绝不开 ForwardAgent;优先走 SSH CA 短证书或宿主侧受控执行器;不要照搬"挂 SSH_AUTH_SOCK"的说法(OpenShell 挂不进去)。
  4. 验收命令
    • OpenShell:openshell logs <sandbox> --tail 看 allow/deny;env 看占位符。
    • DSH:ls -l $DSH_HOME/.credentials.yaml;在 agent 里尝试 cat ~/.dsh/.credentials.yaml 应被沙箱拒绝。

总结