让 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 的默认隔离、以及几个轻量替代方案一起理清。先给结论:
- API key 防泄漏有两条路线:把真 key 放在 agent 够不到的地方(代理网关 / 宿主进程),或让 agent 跑在够不到 key 的沙箱里(内核隔离)。前者防"读进程/文件",后者防"读宿主文件"。
- OpenShell 走的是前者:L7 代理 + 凭证占位符替换,外加文件/网络/进程/凭证四层声明式 policy,dut 上实测可用。但它有一个"占位符进对话体"的坑(§3.4),一旦踩中,会话里所有模型请求都会失败。
- SSH key 是完全不同的问题:私钥本来就不传输,防的是"谁能使用签名能力"。OpenShell 目前没有现成的 ssh-agent 转发/挂载,网上那套
IdentityAgent=$SSH_AUTH_SOCK的配置在 OpenShell 里配不出来(§4.3)。 - 如果只想轻量解决"agent 拿不到 API key":本地代理网关(LiteLLM 之类)把 agent 的 shell 扔进容器/独立用户,比 OpenShell 省事得多(§5)。
一、先分清三层机制
讨论"防泄漏"之前先分清防的是哪一段,否则很容易把三个东西混在一起:
| 层 | 代表机制 | 防住什么 | 防不住什么 |
|---|---|---|---|
| 网络/应用层代理 | 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)正常,只有被污染的会话全挂。
- 根因:源码
l7/rest.rs的emit_uninspected_body_credential_denial(findingopenshell.credentials.traffic_uninspectable)。请求体里的凭证占位符无法安全检查/重写,默认 fail-closed。 - 修法:把占位符从会话历史里清掉(
/reset开新会话,或清理 state.db 后让会话缓存重载)。 - 别急着开
request_body_credential_rewrite: true:我们的open_internet是 hostless(任意主机)端点,开了等于把真 key 替换进发往任意主机的请求体,安全上不划算。 - 教训:agent 永远不要读自己的 key/占位符(
env | grep、cat .env)。对 agent 来说,那段字符串看起来"不是 secret",但它会让整个会话失去与外界的模型通信。
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 深度定制。
可行的路线,按推荐程度排:
- 短时 SSH 证书(step-ca / Vault / Teleport 之类的 SSH CA):给 agent 发分钟级/小时级证书。证书在沙箱里,但泄漏窗口极小、可撤销、可绑定主体。这是"让 agent 能 SSH"最稳的做法。
- 宿主侧受控执行器:私钥和 agent 留在宿主机,把"固定几条 ssh 命令"包成一个受控服务/脚本,通过 policy 放行的端口给沙箱调用。agent 只有"请求执行"的能力,没有 key。
- 专用受限 key 放沙箱:用远端
authorized_keys的command=/from=/no-pty/no-port-forwarding约束死。实现最简单,但等于放弃"agent 看不到 key",只适合低价值目标。 - 不让沙箱做 SSH:这类操作留在宿主或 CI,agent 只要结果。
四、DSH(DeepSeek Harness)的默认隔离
这一节主要整理自公开资料,未在本机逐项实测;本机的 DSH 部署形态见caddy 反代 DSH Web。配置前建议以实际版本的文档/行为为准。
- 内核级沙箱:Linux 用 bwrap(namespace 拼文件系统视图)+ Landlock(内核强制访问控制);macOS 用 Seatbelt;Windows 用 ACL 受限令牌;隔离不可用时 fail-closed(宁可拒绝执行,不悄悄降级)。
- 默认 workspace-write:agent(含 shell 工具)在沙箱内运行,只能在工作区目录内写;工作区之外按只读视图挂载,读得到代码/调试工具,改不了系统文件。
- 凭据只写不回读:Web UI 里填的 API key 写入
$DSH_HOME/.credentials.yaml,前端只显示脱敏描述符。 - 默认只监听回环:
127.0.0.1,CLI 默认拒绝--host 0.0.0.0(我们这台就是 caddy 反代 + basicauth 暴露,配置平面另有 loopback 围栏)。 - 边界:
.credentials.yaml是同用户 600 文件,同一 OS 用户下的其他进程理论上仍可读(以及/proc/<pid>/environ)。如果威胁模型包含"agent 逃逸出沙箱后以同用户身份活动",需要把 DSH 跑在独立用户或容器里。
五、轻量方案对比
| 方案 | 复杂度 | 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 破坏宿主文件。选方案前先写清楚威胁模型。
六、照着配置的清单
- 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。
- 验证占位符:
- 宿主 DSH
- 确认默认沙箱生效(workspace-write);
.credentials.yaml权限 600;威胁模型包含同用户逃逸就换独立用户/容器。
- 确认默认沙箱生效(workspace-write);
- SSH
- 绝不开
ForwardAgent;优先走 SSH CA 短证书或宿主侧受控执行器;不要照搬"挂SSH_AUTH_SOCK"的说法(OpenShell 挂不进去)。
- 绝不开
- 验收命令
- OpenShell:
openshell logs <sandbox> --tail看 allow/deny;env看占位符。 - DSH:
ls -l $DSH_HOME/.credentials.yaml;在 agent 里尝试cat ~/.dsh/.credentials.yaml应被沙箱拒绝。
- OpenShell:
总结
- API key 防泄漏的关键不是"藏起来",而是让真值不进入 agent 进程:要么代理网关侧替换(OpenShell、LiteLLM),要么把 agent 关进内核沙箱(DSH、bwrap+Landlock)。
- OpenShell 的能力是"凭证注入 + 策略 + 审计"三合一,实测有效,但要接受它的复杂度,并注意占位符不能进入对话体这个坑。
- SSH key 是密码学层面的问题,防的是签名能力滥用而非传输截获;想在沙箱里"用 SSH 但看不到私钥",OpenShell 目前没有现成方案,优先考虑 SSH CA 短证书或宿主侧受控执行器。
- 二手资料里"OpenShell 挂宿主工作目录 / 命令在宿主执行"之类的说法来自别家产品(OpenClaw),照着配会踩空——以实测和官方 policy schema 为准。