☰
OpenShell 治理拦截器示例:用 Gateway Interceptor 构建签名的策略基线与权威 Provider Profile 源
2026/9/25 7:20:58 网站建设 项目流程

【免费下载链接】OpenShell

OpenShell is the safe, private runtime for autonomous AI agents.

项目地址:https://gitcode.com/gh_mirrors/op/OpenShell
点击查看免费下载

本篇以 examples/governance-interceptor 示例为主体,讲解如何基于 OpenShell 的GatewayInterceptorgRPC 服务实现一个独立的治理拦截器:它向网关颁发 provider profile、将网关设置为唯一 profile 来源、为策略基线计算规范化 SHA-256 摘要并签发 EdDSA JWT,并在沙箱创建、策略同步、profile 导入等关键 RPC 上强制执行“只允许治理者签发的策略生效”的闭环。读完本篇,你可以完整复现 smoke.sh 端到端验证流程,并理解签名摘要契约、动态策略重载与“沙箱自改策略”封堵的实现细节。

示例定位与治理规则集

该示例是一个独立的 Rust 工程(见 Cargo.toml,包名openshell-governance-interceptor-example,产物为governance-interceptor与governance-smoke-client两个二进制),实现了openshell.gateway_interceptor.v1.GatewayInterceptor服务(proto 定义位于 proto/gateway_interceptor.proto)。它的核心目标是演示:一个拦截器如何成为网关的权威 provider profile 来源,并把受治理的策略基线强制注入到每一个新沙箱。

README 中列出的治理规则集是理解这个示例的骨架,共 12 条约束:

  • provider profile YAML 放在profiles/*.yaml;
  • profile list只显示该拦截器颁发的(vended)profile;
  • 只有type命中已颁发 profile ID 的 provider 才能被创建;
  • 每个颁发的 profile 都带有 hash、签名、签名 key ID 三个治理注解;
  • 每个新沙箱在CreateSandbox期间都会收到policy.yaml;
  • 请求的沙箱 provider 必须匹配某个已颁发的 profile ID;
  • 每个新沙箱获得openshell.nvidia.com/policy-signature元数据注解,用于后续验证策略;
  • CreateSandbox的评估结果会附加correlation_id日志注解(供网关审计日志)以及非机密的策略 hash / 签名 key 元数据;
  • 沙箱策略同步必须携带当前已签名的治理策略,未签名、过期或被篡改的策略对所有调用者一律拒绝;
  • 沙箱提交的策略分析可以携带遥测,但沙箱自行撰写的策略提案(proposed chunks)在进入网关 handler 前即被拒绝;
  • proposal_approval_mode=auto在沙箱与全局两个作用域都被阻断;
  • 用户无法导入或更新已颁发集合之外的 profile;profile 删除被拦截器阻断。

这些规则并非空话,每一条都能在 src/main.rs 的evaluate_inner分派逻辑(约 L349-L403)中找到对应的验证函数:CreateSandbox的 modify/validate 两阶段、CreateProvider、UpdateConfig、SubmitPolicyAnalysis、ImportProviderProfiles、UpdateProviderProfiles、DeleteProviderProfile七个绑定各对应一个validate_*实现。

运行示例

直接运行拦截器

在示例目录下手动启动:

cargo run -- \ --listen 127.0.0.1:18081 \ --policy policy.yaml \ --profiles profiles \ --gateway-endpoint http://127.0.0.1:8080

从 src/main.rs 的参数解析看,完整参数集为:

参数作用默认值
--listen ADDR拦截器 gRPC 监听地址127.0.0.1:18081
--policy FILE治理策略 YAML 路径示例目录下的policy.yaml
--profiles FILE_OR_DIRprofile 文件或目录示例目录下的profiles/
--gateway-endpoint URL网关端点,用于策略重载后向运行中沙箱传播无(不传播)
--policy-watch-interval-ms MS策略文件轮询间隔(毫秒),必须大于 01000(常量DEFAULT_POLICY_WATCH_INTERVAL_MS,main.rs L62)

用 smoke.sh 一键拉起网关 + 拦截器

smoke.sh 会构建openshell-gateway、openshellCLI 和拦截器三个二进制,自动在 20000-39999 区间挑选三个连续空闲端口(拦截器 / 网关 / 健康检查),用openssl genpkey -algorithm ed25519生成网关签名密钥对,写出临时gateway.toml,然后启动拦截器(--policy-watch-interval-ms 250)与网关(SQLite 库、--disable-tls):

./smoke.sh # 交互式:打印端点与日志路径后常驻,Ctrl-C 退出 ./smoke.sh --test-suite # 运行治理冒烟测试套件,完成后停止网关

失败时脚本会保留临时目录并 dump 出 setup / gateway / interceptor 三份日志(见脚本中cleanup与dump_logs)。

网关侧配置:让拦截器成为唯一 profile 来源

README 给出的网关 TOML 片段是该示例的控制面核心,完整继承如下:

[openshell.gateway] provider_profile_sources = [ { type = "interceptor", name = "provider-governance" }, ] [[openshell.gateway.interceptors]] name = "provider-governance" grpc_endpoint = "http://127.0.0.1:18081" order = 10 failure_policy = "fail_closed" binding_policy = "allowlist" timeout = "500ms" max_response_bytes = 1048576 max_patches = 32 [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/CreateSandbox" phases = ["modify_operation", "validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/CreateProvider" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/UpdateConfig" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/SubmitPolicyAnalysis" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/ImportProviderProfiles" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/UpdateProviderProfiles" phases = ["validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/DeleteProviderProfile" phases = ["validate"]

关键点:

  • provider_profile_sources只配置了{ type = "interceptor", name = "provider-governance" }这一项,即用户源(user source)被整体省略——因此用户导入的 profile 不会出现在目录里,profile list只展示github和slack;
  • failure_policy = "fail_closed"与拦截器 manifest 中每个 binding 自带的fail_closed一致(main.rs L590-L601):拦截器不可用时,受绑定的 RPC 直接失败而不是放行;
  • binding_policy = "allowlist"表示只有显式列出的 RPC/phase 组合才会被路由到该拦截器。

smoke.sh写出的完整配置还包含[openshell.gateway.auth] allow_unauthenticated_users = true以及[openshell.gateway.gateway_jwt]段(signing_key_path/public_key_path/kid_path/gateway_id),后者是冒烟测试中为沙箱身份铸造网关签名 JWT 所必需的,见下文“冒烟测试套件”一节。

签名策略基线:规范化摘要 + EdDSA JWT

这是示例的技术核心。启动时(load_policy_state,main.rs L801-L818):

  1. 用openshell_policy::parse_sandbox_policy解析policy.yaml得到SandboxPolicyproto;
  2. 通过本地反射的 protobuf JSON codec(src/proto_json.rs,类型名openshell.sandbox.v1.SandboxPolicy)渲染成 ProtoJSON;
  3. 对规范化 JSON 计算 canonical SHA-256 摘要;
  4. 以 EdDSA 算法把摘要签进一个 JWT(sign_policy,main.rs L124-L138)。

摘要契约:sha256:v2:

README 明确说明:这个摘要契约由示例独立于网关拥有。其实现集中在 src/policy_hash.rs:

  • 算法标识常量为HASH_ALGORITHM = "openshell-governance-protojson-sha256-v2",摘要格式为sha256:v2:<hex>(policy_hash.rs L11-L16);
  • 规范化 JSON 序列化递归地对对象键按字节序排序,但保留 repeated 字段(数组)的顺序(write_canonical_json,policy_hash.rs L90-L121)——即 map 无序、数组有序;
  • 哈希输入采用长度前缀 framing(hash_framed)串联:算法标识、领域串(如openshell-governance-policy)、proto 类型名、规范化 JSON 字节,防止拼接歧义;
  • profile 快照 revision(canonical_profile_snapshot_revision)则把每个 profile 按 ID 字节序排序后整体哈希,使快照 revision 对 profile 顺序不敏感。

单元测试 policy_hash.rs L151-L222 精确验证了这三条性质:policy_hash_is_recursive_and_preserves_repeated_order断言“嵌套 map 反序同 hash、数组反序不同 hash”;profile_hash_and_snapshot_revision_ignore_map_and_profile_order断言快照 revision 忽略 profile 顺序;digest_format_is_explicitly_v2断言sha256:v2:前缀与 64 位小写十六进制。

注意 README 的一句重要澄清:网关自身的 policy hash 是另一个运维意义上的修订标识符,不要求与这里的签名治理 hash 相等。

JWT 与签名密钥

签名密钥(Ed25519,经rcgen生成)在每次拦截器启动时在内存中生成,kid取公钥 DER 的 SHA-256 前 16 字节(kid_from_public_key_der,main.rs L1062-L1065)。策略 JWT 的固定 claim 为:sub=policy.yaml、iss=openshell-governance-interceptor、aud=openshell-governance-policy、hash_algorithm、policy_sha256(常量见 main.rs L53-L57)。验证逻辑verify_policy_signature(main.rs L157-L189)会校验 kid、EdDSA 算法、iss/aud、sub、hash_algorithm以及policy_sha256与当前策略哈希逐字相等。

README 同时给出生产化提醒:生产治理服务应加载受管的签名密钥、发布验签公钥并定义密钥轮换流程;本示例为自包含而选择内存密钥。

CreateSandbox 两阶段:注入与验证

CreateSandbox是唯一绑定两个 phase 的 RPC(modify_operation+validate),对应 main.rs 的两个分支:

modify_operation(patch_create_sandbox,L405-L442):以 RFC 6902 JSON Patch 形式改写请求——

  • add /spec/policy:把治理基线(ProtoJSON 形态的完整策略)写入请求 spec;
  • add /annotations/openshell.nvidia.com/policy-signature:写入策略 JWT(若无 annotations 对象则整体创建);
  • 同时向log_annotations注入correlation_id = governance:create-sandbox:<沙箱名>、policy_hash、policy_signature_kid,供网关审计日志落盘(smoke 套件用expect_log_contains断言网关日志中出现该 correlation id)。

validate(validate_create_sandbox,L633-L663):逐项复核——

  1. /spec/policy必须存在,否则拒绝“sandbox policy must match the provider governance baseline”;
  2. annotations中必须有policy-signature;
  3. 把请求里的 policy 反解回SandboxPolicy,重算 canonical hash,验证 JWT 签名(validate_signed_policy_payload,L665-L682);
  4. hash 必须等于拦截器当前基线 hash,且反解出的 proto 必须与基线 proto 逐字段相等——这意味着沙箱在 modify 阶段拿到的策略若被任何调用方在 validate 前篡改,都会被拒;
  5. /spec/providers中的每个 provider 必须是已颁发的 profile ID。

示例策略基线见 policy.yaml:文件系统策略(read_only覆盖/usr、/lib等,read_write为/sandbox、/tmp)、landlock: best_effort,以及一条my_api网络策略放行api-1.example.com:443(REST,enforce,full 访问)。

动态策略与 profile 重载

策略文件轮询与传播

拦截器默认每 1 秒轮询一次policy.yaml。看门狗实现为spawn_policy_watch_worker(main.rs L1162-L1221):

  • 以(mtime, len)文件指纹判断变化,避免无谓重读;
  • 文件变化且解析成功时调用reload_policy_from_yaml:重新计算 hash 并重签;若新 hash 与旧 hash 相同则不产生新版本(L475-L486);
  • 解析失败时打印错误并保留上一份有效策略;
  • 配置了--gateway-endpoint时,随后执行propagate_policy_to_running_sandboxes(L1231-L1303):分页ListSandboxes,对处于Ready/Provisioning阶段的沙箱逐个调用UpdateConfig,携带expected_resource_version(乐观锁)与四个治理注解——policy-signature、policy-hash、policy-signature-kid、policy-reload-correlation-id(policy_update_annotations,L1323-L1345),使动态变更走网关正常的沙箱配置轮询通道被沙箱感知;
  • 网关拒绝的“静态基线变更”(InvalidArgument)只记录日志,但新策略仍会应用于新创建的沙箱。

未配置--gateway-endpoint时,重载仅对后续CreateSandbox生效,启动日志会打印提示(smoke.sh 中拦截器始终以网关端点启动)。

Profile 快照重载

current_profile_state(main.rs L444-L466)在每次评估时重新读取--profiles目录:加载并签名成功则原子替换快照;失败则打印 "keeping last valid snapshot" 并沿用上一份有效快照。由于 profile 内容变化会改变其签名与快照 revision,运行中沙箱会经由网关配置轮询路径重新加载 provider 派生的有效策略。

Provider Profile 颁发机制

文件即 ID 的加载规则

load_provider_profiles(main.rs L833-L860)支持文件或目录两种形态:目录模式下仅接受.yaml/.yml扩展名,按路径排序加载并要求至少一个文件、文件名 stem 不重复。Profile ID取自文件名去掉扩展名的部分(profile_id_from_file_name,L887-L910):profiles/github.yaml→ IDgithub;stem 必须是已规范化的 lowercase kebab-case。YAML 中的id字段会被强制覆盖为文件名(load_provider_profile_source中mapping.insert("id", ...),L870-L885),即使存在也以文件名优先。

示例自带两个 profile:

  • profiles/github.yaml:category: source_control,凭证api_token(从GITHUB_TOKEN/GH_TOKEN读取,bearer 方式),端点覆盖api-1.github.com、api.github.com/graphql(GraphQL)、github.com,全部read-only+enforce,允许二进制gh、git;
  • profiles/slack.yaml:category: messaging,Slack Web API 只读,用显式 L7 allow 规则限定GET /api/team.info、/api/users.info、/api/conversations.info、/api/conversations.history四条路径。

签名注解与快照服务

每个 profile 在装入快照前都会先剥离旧的三个签名注解,重算确定性 hash,再写回(sign_provider_profile/deterministic_profile_hash,main.rs L962-L991):

注解内容
openshell.nvidia.com/profile-signatureprofile 规范化 payload 的 EdDSA JWT(sub=provider-profile:<id>,aud=openshell-governance-profile)
openshell.nvidia.com/profile-hashsha256:v2:<hex>摘要
openshell.nvidia.com/profile-signature-kid签名 key ID

拦截器在 manifest 中声明provider_profiles = true(main.rs L294-L347),网关随后通过SnapshotProviderProfiles拉取当前{ revision, profiles }。README 强调:这些注解只是演示“拦截器可以拥有的逻辑”,网关把它们当作不透明元数据,不会自行验证。

四个 profile 管理 RPC 的强制策略

RPC验证行为(源码位置)
CreateProviderprovider.type必须命中已颁发 ID,否则PERMISSION_DENIED(validate_create_provider,L488-L501)
ImportProviderProfiles请求必须携带 profile 载荷,且每个profile.id都必须在已颁发集合内(validate_import_provider_profiles,L503-L524)
UpdateProviderProfiles目标id必须受管,且载荷 ID 必须与目标 ID 一致(validate_update_provider_profiles,L526-L551)
DeleteProviderProfile无条件拒绝:"provider profile deletes are blocked by provider governance"(validate_delete_provider_profile,L797-L799)

封堵沙箱“自改策略”的两道闸

UpdateConfig:签名策略同步 + 阻断自动批准

validate_update_config(main.rs L684-L714)的处理顺序:

  1. 若请求设置proposal_approval_mode=auto(兼容settingKey/setting_key与stringValue/string_value两种 JSON 拼写,requests_auto_proposal_approval,L716-L735),直接拒绝——该函数不区分global标志,因此沙箱级与全局级两个作用域的 auto 都被阻断;
  2. 非全局请求携带policy时进入validate_update_config_policy(L757-L795):必须同时携带 policy 载荷与三个治理注解(signature/hash/kid),注解值必须与拦截器当前基线逐字一致(stale 即拒),最后再做完整的 JWT 验签 + hash + proto 全等三重校验。未签名、过期或篡改的策略对所有调用者拒绝;
  3. 非全局请求携带mergeOperations时直接拒绝("sandbox policy updates are blocked...");其余 UpdateConfig 放行。

SubmitPolicyAnalysis:遥测放行、提案拒绝

validate_submit_policy_analysis(main.rs L737-L755):

  • 调用方 principal 的kind必须为sandbox,否则拒绝(策略分析要求已认证的沙箱主体);
  • proposedChunks非空数组 → 拒绝 "sandbox-authored policy proposals are blocked",堵死了沙箱利用网关可选的 auto-approval 路径放宽治理策略;
  • proposedChunks为空或缺失(仅带network_activity_summaries等遥测)→ 放行。

README 特别注明:这条规则属于示例自身的治理策略;未部署该绑定的网关仍保留标准的 proposal 工作流。

冒烟测试套件:端到端验证闭环

./smoke.sh --test-suite的套件(smoke.sh run_suite,L440-L624)按顺序验证了治理闭环的每个断言:

  1. profile list只含github/slack;向用户源--global导入的unvended-apiprofile 不出现在目录中(用户源被省略的直接效果);profile export github -o json含profile-signature与profile-hash注解;
  2. profile delete slack与导入越权 profile(custom-slack)均失败;
  3. provider create --type github/--type slack成功,--type bitbucket失败;
  4. settings set --global --key proposal_approval_mode --value auto失败(全局作用域阻断);
  5. sandbox create --provider github --no-auto-providers成功:网关日志含log_annotations与governance:create-sandbox:<name>;沙箱 provider 列表只有 github;有效策略含_provider_github层而无_provider_slack层;
  6. 运行governance-smoke-client(见下)验证认证越权被拒且策略不变;
  7. 覆写临时policy.yaml(换成example_api规则)→ 等待policy get出现新规则、沙箱日志出现新 hash、policy-signature注解变化(证明重签与传播);
  8. 覆写profiles/github.yaml(新增profile-reload.example端点)→ 等待 profile 导出与沙箱有效策略出现新端点、profile-signature变化;
  9. 最后policy set替换策略被拒绝(未携带当前签名注解的替换即 stale/unsigned),并删除沙箱收尾。

其中第 6 步的 src/smoke_client.rs 是专门构造“带合法身份的攻击者”的负路径客户端:它用网关签名密钥对铸造一个沙箱身份 JWT(sub=spiffe://openshell/sandbox/<id>,iss=openshell-gateway:<gateway_id>,对应网关配置中的gateway_jwt段),然后:

  • 复制当前策略、插入一条新网络规则后调UpdateConfig—— 期望PermissionDenied(未签名策略放宽);
  • 用analysis_mode=agent_authored提交一条proposed_chunks—— 期望PermissionDenied(沙箱自撰提案);
  • 提交仅含network_activity_summaries的activity模式分析 —— 必须成功(遥测放行);
  • 最后比对GetSandboxConfig前后的version与policy_hash,断言两次拒绝都没有改动生效策略(smoke_client.rs L125-L140)。

这正对应 README 末段的描述:套件使用网关签名 JWT 扮演被治理沙箱身份,尝试未签名放宽与策略提案,确认两者被拒、遥测被接受、策略版本与 hash 不变。

复用与延伸阅读

从源码结构看,该示例刻意只依赖工作区内的openshell-core(proto 与扩展协议元数据openshell/provider-governance)、openshell-policy(YAML → proto 解析)、openshell-providers(profile 模型与 ID 规范化,crates/openshell-providers),没有复制网关内部逻辑,因此适合作为生产治理服务的骨架:

  • 保留“文件名即 ID + 强制受管集合 + 删除阻断”的 profile 治理模型,把内存密钥替换为受管签名密钥并发布验签公钥;
  • 保留sha256:v2:摘要契约与 framing 细节(src/policy_hash.rs),跨服务验证时双方必须使用同一hash_algorithm;
  • 把fail_closed+allowlist的绑定风格作为拦截器配置的基线,参考仓库中 docs/extensibility/gateway-interceptors.mdx 的拦截器协议文档与 proto/gateway_interceptor.proto 的服务定义;
  • 单元测试入口在 src/tests.rs 与 src/policy_hash.rs 的#[cfg(test)]模块,cargo test(示例独立 workspace)即可离线验证摘要与验签逻辑,无需启动网关。

需要注意的适用前提:示例的签名密钥每次重启都会变化,因此拦截器重启后旧沙箱上的policy-signature注解将无法通过新进程验证,动态传播的UpdateConfig会因 stale 注解被自己的校验拒绝——这正是 README 建议生产环境引入密钥轮换流程的原因。

【免费下载链接】OpenShell

OpenShell is the safe, private runtime for autonomous AI agents.

项目地址:https://gitcode.com/gh_mirrors/op/OpenShell
点击查看免费下载

相关推荐

上一篇:终极指南:如何使用 browser-use-mcp-server 实现AI驱动的浏览器自动化
下一篇:Android应用脱壳实战:AndroidSecNotes揭秘常见加固厂商破解方法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询