bbo777/dsh-baseline

dsh-baseline

A fail-closed security baseline plugin for DeepSeek Harness (DSH): policy-gated network egress, a monotonic subagent permission ceiling, and one hash-chained audit log of every tool decision and result.

待分类auditdeepseek-harnessdsh

dsh-baseline

DSH 的安全基线闸门:网络出口独立成层,子代理权限只能收窄不能放宽,每一次判定与结果写进同一条可校验的哈希链。

默认行为一句话:公网主机放行;私网、回环、链路本地、CGNAT 与云元数据地址默认阻断。配置 allowedHosts 后转为白名单模式,白名单外的公网目标需审批(strict 下直接拒绝)。

版本 0.1.0 · DSH peer >=0.2.1-rc.2 = 22.5(node:sqlite;22.13 起无需额外 flag)
Host 半区与客户端均已实现;真机验证范围见验证状态与待验证项:桌面版验证服务端加载、审计、链与 HTTP 路由,CLI Web 验证客户端渲染、主题与 API。

快速开始

dsh plugin --profile web add dsh-baseline

装好后重启 DSH,左侧栏出现「基线审计」即生效。桌面版请在 Plugin Manager 里安装 dsh-baseline。

本包安装即启用。若想先审查再启用,见安装与启用第 3 条。

首屏:五条不能含糊的话

1. 不魔改 DSH 源码

只使用官方公开的扩展点和事件:tools/pre-execute、tools/result、ctx.tools.guard()、ctx.approval、ctx.agents、webServer.register()、客户端 slots / locale。不依赖未文档化的内部 API,不替换、不补丁、不包装 DSH 自身的运行时文件。

2. fail-closed

任何不确定的判定、审批缺失、审批超时、子代理 lineage 断裂、审计库不可用,一律拒绝,并给出明确原因。宁可让一次调用失败,也不让它在无法验证时继续执行。

3. 与相邻产品互补,可共存

  • 与 dsh-permgate:互补。它做 8 类权限拦截;本插件不替代权限分类,只守网络、子代理上限、凭据与破坏性操作这几条底线。
  • 与 auditrail:互补。它做 SQLite 哈希链审计;本插件的链只记录自身的判定与结果,不接管全局审计。
  • 与 dsh-plugin-product-subagents:互补。它提供子代理上限;本插件在 ctx.tools.guard() 里再保一次单调上限,任何后注册的 listener 或 guard 都不能把子代理放宽。
  • 与 dsh-plugin-vet:互补。它做运行时观察;本插件做阻断、审批与可校验审计。

4. 能力边界:明确不覆盖什么

  • 不覆盖“恶意插件直接 require('child_process')”的逃逸。纯插件层看不到绕过 ToolRuntime 的原生进程调用,这需要进程级隔离、容器或操作系统策略。
  • 不覆盖 DSH Code Mode run_code 的 worker thread 逃逸。worker 内执行的代码不经过可拦截的工具调用边界,同样需要进程级隔离。
  • 不解析 DNS。公网域名解析到 127.0.0.1 或发生 DNS rebinding 时,本插件只能看到主机名字符串。
  • 不是防火墙,不是沙箱,不是杀毒软件,也不是完整的能力权限系统。完整清单见能力边界。

5. “baseline”的含义:有些底线,任何配置都不能推翻

启用插件后,以下两类规则无条件、优先于所有自定义规则执行,配置文件和面板都无法关闭:

  1. 破坏性系统命令(根目录递归删除、格式化磁盘、写块设备、fork bomb 等);
  2. 对设备或内核接口的原始写入(/dev/sd*、/proc/self/mem、/sys/*、\.\PhysicalDrive*)。

网络底线分两部分:

  • 不可关闭:始终阻断的内部主机名与后缀(云元数据、.internal、.svc、.local 等);
  • 默认开启、可运营调整:私网、回环、链路本地、CGNAT 的 IP 段由 blockedCIDRs 定义。默认列表覆盖这些范围;删除条目会明确削弱该范围的阻断,这是操作者的显式选择,README 不把它伪装成不可推翻。

凭据读取/外发拒绝与子代理权限上限默认开启,只能通过配置文件中的 guard 开关显式关闭,面板不能关闭。

“默认开启”指策略在安装后即生效;本包安装即启用(见安装与启用)。如需先审查再启用,见该节第 3 条。

它解决什么问题

大多数 Agent 安全产品在“工具名”层面做权限控制。但真实的逃逸往往发生在三个地方:网络出口、子代理继承、以及“谁在什么时候基于什么理由允许了什么”。dsh-baseline 把这三处做成同一套可验证的机制:

层 · 机制 · 不可绕过的原因 网络出口 · 私网/元数据默认阻断、可选主机白名单、按会话出口预算、严格模式逐个审批 · 网络命令与 URL 在 pre-execute 中提取,硬规则在 guard 中二次复核 子代理上限 · 沿 session.header.parentSession 回溯祖先,重放同一次调用的策略 · 在 ctx.tools.guard() 中执行;guard 只有“拒绝/不干预”两种结果,没有 allow,后注册者无法放宽。fork 会话不计入 审计 · 判定与结果写入同一条 SHA-256 哈希链,客户端只读查询 · 写入前脱敏;verifyChain() 返回第一条断链记录;客户端不直接接触 SQLite

它不做的:

  • 不替代 dsh-permgate 的权限类别;
  • 不把每条审计记录上传到远端;
  • 不读取业务数据,不保存文件内容、patch、diff、图片或消息体;
  • 不提供自定义审批 UI,审批一律复用 DSH 标准 ctx.approval;
  • 不在客户端保存 token,不使用 localStorage。

安装与启用

前置条件

  • DSH >=0.2.1-rc.2 <0.3.0(peerDependencies 与 engines.dsh 的区间;见下方兼容矩阵)
  • Node.js >= 22.5,且 Node 带 node:sqlite(本包使用 DatabaseSync)。22.13 起无需额外 flag;22.5–22.12 需要以 --experimental-sqlite 启动 DSH
  • DSH 以 Web 模式运行(只有 Web 模式会挂载 webServer;面板与 HTTP API 依赖它)

兼容矩阵

DSH · Cordis · 状态 0.2.1-rc.3 · 4.x · 已验证(桌面版:加载 / deny / 审批拒绝 / 审计链 / HTTP 路由) 0.2.1-rc.2 · 4.0.5 · 已验证(CLI Web:客户端渲染 / API 全路由)+ 服务端真实 ToolRuntime 集成测试 其它 0.2.x · 4.x · 未验证(peer 与 engines.dsh 允许,但未跑过)

package.json 的 dsh.compatibility.dsh 记录首个完成真机验证的版本(0.2.1-rc.2),不代表只支持该版本;实际允许范围由 peerDependencies['@deepseek-ai/dsh-tools'] 与 engines.dsh 的区间决定。

构建

npm install
npm run build

产物:

  • lib/index.js 与 lib/types/:Host 半区
  • lib/client.js:客户端 bundle(约 60 KB)
  • locale/zh.json、locale/en.json:面板与 Plugin Manager 的显示元数据
  • icon.svg:插件图标

安装

  1. 在 DSH 的 Plugin Manager 中安装 dsh-baseline(从 npm),或使用 install_bundle 安装本目录 / npm pack 后的 tarball。不要手工复制 node_modules 或改写 Loader 配置来替代安装步骤。
注:已在 DSH 0.2.1-rc.3 desktop 上通过 Plugin Manager 从 npm 安装并验证(侧栏入口、面板、导出、熔断解除均通过)。
  1. 安装后先看 bundle 详情与插件行:标题应为“安全基线”/Security baseline,描述与图标来自 locale/*.json 与 icon.svg。
  2. 本包安装即启用。 随包发布的 cordis.patch.yml 只做一次 insert,不带 disabled 键:
- insert:
       - id: dsh-baseline
         name: dsh-baseline
         config: {}

原因:Plugin Manager 的安装开关只会把包写进 dsh.profile.bundles,不会翻转条目的 disabled。早期版本把 disabled: true 写死在包里,结果是「装上了、列表里有、重启后什么都没发生」——插件被合成进树里,却从未实例化。

想先审查再启用,不要改包内文件,而是在 profile 的 cordis.patch.yml 末尾加一行覆盖:

- id: dsh-baseline
     disabled: true

审查完改成 false 或删掉该行,再重启 DSH。

  1. 替换已安装的包后需要重启 DSH,才能加载新的 JavaScript 模块;仅安装新 bundle 可能通过 HMR 生效。
  2. 重启后打开 Web UI:左侧栏出现“基线审计”图标,主面板 key 为 baseline。面板需要 Host 半区已启用且 webserver/index-inject 已注入启动 token。

配置覆盖语义

DSH Loader 中,匹配到同一插件的 config 会整体替换原有配置,而不是逐字段合并。Schemastery 会为缺失字段补默认值,但不会保留你上一次写的值。覆盖配置时,请把需要偏离默认值的字段一次写全。

推荐的最小启用配置:

- insert:
    - id: dsh-baseline
      name: dsh-baseline
      disabled: false
      config:
        defaultMode: standard

策略引擎

三种模式

行为 · loose · standard(默认) · strict 硬规则(含凭据/网络) · 执行 · 执行 · 执行 自定义规则 · 执行 · 执行 · 执行 内置审批规则 · 不触发 · 触发 · 触发 严格模式兜底 · 不启用 · 不启用 · 启用 无规则命中时的默认 · 允许 · 允许 · 只读放行;网络或可能修改状态的调用需审批 熔断 / 出口预算 / 审计降级 · fail-closed · fail-closed · fail-closed 子代理上限(启用时) · 执行 · 执行 · 执行

standard 是低误报默认:只在有明确信号时弹审批(下载管道执行、破坏性 Git、破坏性 SQL、持久化、监听端口、读 .env、写系统目录、证据不完整,以及配置了白名单后的白名单外目标)。

硬规则(优先于自定义规则)

这些规则在自定义规则之前评估,effect 固定为 deny,risk 为 critical(出口预算的 guard 拒绝为 high)。

guard-private-network 的规则本身不能被配置或面板关闭,但其 IP 段集合来自 blockedCIDRs,删除某个 CIDR 会使其不再命中;始终阻断的内部主机名不受列表影响。凭据类规则受 guard.enforceCredentialDeny 控制。

规则 ID · 触发条件 guard-destructive-command · 递归删除根目录/home(操作数先做 POSIX 规范化,/tmp/..、/./、// 视为根);删除盘符根;format 磁盘;dd 写块设备;mkfs* / wipefs / blkdiscard;对设备 shred;在根目标递归 chmod/chown;在根目标 find -delete/-exec;fork bomb guard-root-write · 任意命令或工具的写入目标规范化后等于文件系统根(/、C:、//)或当前用户主目录(~、$HOME);覆盖 mv x /、> / 等非 rm 变体 guard-device-write · 写 /dev/sd*、/dev/nvme*、/proc/self/mem、/sys/*、.\PhysicalDrive* 等设备或内核接口 guard-credential-read · 读真正的凭据/密钥:~/.ssh/ 下的 id_rsa/id_dsa/id_ecdsa/id_ed25519 与 *.pem/*.key/*.p12/*.pfx/*.jks/*.keystore,以及 .npmrc、.netrc、.pgpass、~/.aws/credentials、~/.azure/、~/.kube/config、~/.docker/config.json、~/.git-credentials、~/.gnupg/、gcloud 凭据、~/.password-store/。~/.ssh/config 仅审批(见 builtin-credential-config-read);known_hosts 与 *.pub 不拒绝 guard-credential-write · 写入任意 ~/.ssh/authorized_keys(持久化后门) guard-credential-exfiltration · 同一次调用既读凭据又访问网络或写私钥;或读取 .env/敏感配置同时访问网络(即使 loose 模式也拒绝) guard-private-network · 目标命中 blockedCIDRs,或命中始终阻断的内部主机名(见网络策略)

凭据三条规则只在 guard.enforceCredentialDeny: true(默认)时执行。其余四条不可关闭。

内置审批规则

规则 ID · 触发条件 · standard · strict builtin-pipe-to-shell · 下载内容直接管道给 sh/bash/python/node 等解释器;PowerShell irm \ · iex · 审批 · 审批 builtin-destructive-git · git push --force、reset --hard、clean -f/-x、checkout/restore .、branch -d、stash drop/clear、filter-branch/filter-repo · 审批 · 审批 builtin-destructive-database · DROP TABLE/DATABASE、TRUNCATE TABLE、ALTER TABLE ... DROP、无 WHERE 的 DELETE FROM · 审批 · 审批 builtin-persistence · crontab 写入、systemctl enable、launchctl load、schtasks /create、注册表 Run 自启动、chkconfig --add、update-rc.d,以及写入 shell rc/systemd/cron 等持久化位置;authorized_keys 写入由 guard-credential-write 直接拒绝 · 审批 · 审批 builtin-listener · nc -l、socat ... listen、ssh -R、python -m http.server、php -S、ngrok/cloudflared/chisel/frpc · 审批 · 审批 builtin-sensitive-read · 读取 .env / .env.*(.example、.sample、.template、.dist、.md 除外) · 审批 · 审批 builtin-credential-config-read · 读取 ~/.ssh/config(可能包含 ProxyCommand);known_hosts 与 *.pub 不触发 · 审批 · 审批 builtin-protected-write · 写 /etc、/usr、/bin、/sbin、/boot、/dev、/proc、/sys、/var/lib、/var/spool、/Library、/System、C:\Windows、C:\Program Files 等受保护位置(/dev/null、/dev/stderr、nul 等无害设备目标除外) · 审批 · 审批 builtin-unverifiable-evidence · 参数遍历触顶(truncated)且涉及网络或写入 · 审批 · 拒绝 builtin-network-unverifiable · 配置了 allowedHosts、调用涉及网络、但无法确定目标主机 · 审批 · 拒绝 builtin-network-not-allowed · 配置了 allowedHosts、目标不在白名单中 · 审批 · 拒绝 builtin-egress-budget · 仅严格模式:maxEgressBytes > 0 且检测到上传标志 · — · 审批 builtin-workspace-escape · 仅严格模式:写入工作区之外的绝对路径 · — · 审批

严格模式兜底(没有规则命中时):

  • 网络调用且所有目标都在显式白名单内 → 允许(default-strict-allow);
  • 其他网络调用 → 审批(builtin-strict-network);
  • 非只读调用 → 审批(builtin-strict-mutation);
  • 只读调用 → 允许(default-allow)。

自定义规则

rules:
  - id: allow-npm-registry
    effect: allow
    priority: 100
    risk: low
    reason: Package installs may reach the npm registry
    tools: [bash]
    commands: ["npm *", "pnpm *"]
    hosts: ["registry.npmjs.org"]

匹配语义:

  • 不同维度之间是 AND:写了 tools 和 hosts,就必须同时满足。
  • 同一维度内部是 OR:hosts 中任意一个命中即可。
  • 未写的维度视为不限制;但至少要写 tools/paths/commands/hosts/agents 之一。
  • tools、agents:大小写不敏感的 glob。agents 匹配 agent.id(会话 ID)。
  • paths:同时按路径 glob 与普通 glob 匹配,覆盖真实路径、词法路径和工作区相对路径;路径模式中 * 不跨 /。
  • commands:默认是“包含”匹配;模式中出现 *、?、[ 时按 glob 匹配。会匹配完整命令与每个解析后的命令段。
  • hosts:主机名 glob 或 CIDR。
  • 排序:priority 降序;相同优先级按声明顺序;第一条命中的规则生效。
  • effect 可为 allow / deny / ask,risk 为 low/medium/high/critical。
  • 硬规则优先于自定义规则;自定义 allow 不能放行破坏性命令、设备写入、命中当前 blockedCIDRs 的网段或已启用的凭据拒绝。
  • 自定义 allow 仍会经过 guard 的子代理上限检查。
  • 规则 ID 必须唯一,长度 ≤128,不能使用 builtin-、guard-、network-、__ 前缀;每个维度最多 64 个模式。

单调 guard:子代理权限上限

ctx.tools.guard() 的契约是“只返回拒绝原因或不干预”,没有 allow。因此它是唯一能保证“后注册的 listener 不能放宽前人的拒绝”的位置。本插件的 guard 按以下顺序检查:

  1. 审计库不可用 → 拒绝(guard-audit-unavailable);
  2. 会话已熔断 → 拒绝(guard-fuse);
  3. 出口预算已超限且本次涉及网络 → 拒绝(guard-egress-budget);
  4. 重新执行全部硬规则(此时已经过审批;审批不能放宽硬规则);
  5. 找不到本次调用的 pre-execute 记录 → 拒绝(guard-policy-bypass,防止绕过判定阶段);
  6. 子代理上限未启用,或本次是根代理 → 放过;
  7. lineage 无法验证 → 拒绝(guard-subagent-ceiling);
  8. 对每个祖先,按祖先身份重放同一次调用的策略;若祖先的判定等级高于子代理本次判定(deny > ask > allow),拒绝(guard-subagent-ceiling)。

lineage 的来源:

  • 从当前 agent 的 session.header.parentSession 向上走;
  • header.origin === 'subagent' 但没有 parentSession → lineage 断裂;
  • ctx.agents.get(parentSession) 找不到父会话 → 断裂;
  • 出现环、或深度超过 32 层 → 断裂;
  • 断裂一律拒绝,不放宽。

这是“对同一次调用重放祖先策略”的单调上限,不是完整的能力集合格。它保证:如果祖先在同一调用上会被更严格地对待,子代理就不能凭自己的配置获得更宽的结果。若 ctx.agents 不可用,所有子代理调用都会被拒绝(fail-closed)。

审批

  • 只使用 DSH 标准 ctx.approval.request(),不渲染自定义阻塞 UI。
  • 只有 allowed-once 视为通过;rejected、cancelled、unavailable 全部拒绝。
  • approvalTimeoutMs(默认 30,000 ms)到期即拒绝,并标记 timedOut;审计结果记为 timeout。
  • apply 时 ctx.approval 不存在只记 warning;真正需要审批而服务仍缺失时,显式拒绝并写明“approval is required but ctx.approval is unavailable”,不会静默等待到超时。
  • 审批通过后 guard 会重新检查硬规则与子代理上限;一次审批只对本次调用有效。

网络策略