mcpField
R
rust-junosmcp

rust-junosmcp

本地服务
github.com安全32 天前更新

关于 rust-junosmcp

rust-junosmcp 是面向网络运维人员的 Rust MCP 服务,用于自动化管理 Juniper Junos 和 SRX 设备。它将 Junos 与 SRX 工具整合,并提供范围授权、审计和受控配置变更。

主要功能

  1. 01Junos 与 SRX 设备操作
  2. 02配置检查与确认提交
  3. 03基于会话池的批量命令
  4. 04文件传输与 Junos 升级
  5. 05令牌范围控制与审计记录
  6. 06SRX 安全检查与更新

适用场景

  1. 01检查设备事实与配置
  2. 02验证或暂存配置变更
  3. 03跨多台路由器运行命令
  4. 04传输文件或升级 Junos

MCP 相关

通过 stdio 或 streamable HTTP 将 Junos 和 SRX 操作作为 MCP 工具提供。HTTP 传输支持带路由器和工具范围的 bearer 令牌认证。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"mechubsec-rustjunosmcp": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mechubsec/rustjunosmcp:2.4.0"
]
}
}
}
启动命令docker run -i --rm ghcr.io/mechubsec/rustjunosmcp:2.4.0

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
ocighcr.io/mechubsec/rustjunosmcp:2.4.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

mechub 标志

rust-junosmcp

使用 Rust 为 Juniper Junos 和 SRX 设备提供的单一 MCP 服务
mechub 项目 — 自主网络安全自动化

非官方/社区项目。 本仓库是一个独立的社区驱动项目,与 Hewlett Packard Enterprise 或 Juniper Networks 无关联,也未获得其认可、赞助或支持。“HPE”“Juniper”“SRX”“JUNOS”“Security Director”和“Juniper Mist”均为其各自所有者的商标,在此仅用于说明本软件可与之互操作。有关这些产品的支持和许可问题,请联系相应厂商。

一个面向 Juniper Junos 和 SRX 设备、使用 Rust 编写的 Model Context Protocol 服务。单个 rust-junosmcp 进程通过一个工具注册表和端点,提供核心 Junos 工具,并默认提供 SRX 安全工作流。它在清单格式和核心工具接口方面可直接替换 Juniper/junos-mcp-server,但采用异步 Rust (rustEZ + rustnetconf) 构建,而非 PyEZ。

稳定性

rust-junosmcp 已 稳定(自 1.0.0 起不再处于 beta 阶段)。以下接口只会在新主版本中发生不兼容变更;任何弃用都会至少提前一个次版本在 CHANGELOG 中公告。

  • **MCP 工具:**名称、输入架构(必需参数及其类型),以及 execute 门面的操作名称。由 tests/stdio_smoke.rs 和 tests/compat_surface_v0_12_0.rs 固定。
  • **工具错误:**结构化正文中的 code 值(invalid_argument、unauthorized、not_found、rate_limited、upstream_unavailable、outcome_unknown)及 retryable 的含义,还有错误文本中的 RJMCP_* 和 [code=...] 标签。
  • **配置:**CLI 标志、JMCP_* 和 MCP_TOOL_MODE 环境变量,以及 devices.json 和 tokens.json 格式,包括令牌工具和设备作用域。
  • **操作:**审计记录字段和 Prometheus 指标名称(由 tests/prometheus_contract.rs 固定)。

不包括:面向用户的消息和日志措辞、自由文本工具输出的形式,以及工作区内部 crate 的 Rust API。仅支持 Linux 平台,可在 LXC 或 Docker 中运行;部署模式请参阅 远程传输 + 身份验证。

超越 Juniper/junos-mcp-server

在 devices.json 和核心工具方面可直接替换 — 还提供许多 Python/PyEZ 服务没有的功能:

  • 更安全的配置 — commit_check_config(验证但绝不提交)、带自动回滚的确认提交,以及用于解除候选配置卡住状态的 discard_candidate。
  • 设备生命周期 — 分阶段执行 upgrade_junos(镜像 → 安装 → 重启 → 验证)、SCP transfer_file/fetch_file、PFE 命令。
  • 规模与体验 — 并行会话池批处理(速度约提升 1.7 倍)、| last N/| count + max_lines/max_bytes 输出上限、router/router_name 别名、Jinja2 模板。
  • 传输与身份验证 — 支持每个令牌配置路由器/工具作用域的 streamable-HTTP、TLS 和 Host 允许列表;上游仅支持 stdio。
  • SRX 工具(由默认的 srx 功能启用)— IDP 和 Application-ID 签名包更新(检查/下载/安装/回滚)、机箱集群运行状况、许可证和安全服务状态、包含机密信息脱敏的 JTAC 捆绑包,以及只读的安全策略/地址簿/应用/NAT 规则检查。

性能

于 2026-10-05 使用 vSRX 实验室设备(Junos 26.2R1.7)进行基准测试,对比 Juniper/junos-mcp-server 1.1.1 和 shigechika/junos-mcp 0.18.0 / 0.22.0;使用相同的只读工作负载,每项操作运行 3 次、每次调用 30 次,失败调用数为 0。 各次运行的中位数:

rust-junosmcp 0.27.2Juniper 1.1.1shigechika 0.22.0
冷启动6 ms264 ms392 ms
峰值内存(RSS)20 MiB129 MiB98 MiB
每次运行的 SSH 连接数11211
show version,p50216 ms878 ms228 ms
配置 RPC(访问被拒绝的回复¹),p5011 ms776 ms21 ms

¹ 基准测试所用的只读登录账号没有配置视图,因此此项测量的是一次往返,回复约 400 字节,而非完整获取配置。参见 docs/BENCHMARKS.md。

完整结果、模拟目标开销数据、方法和复现步骤: docs/BENCHMARKS.md。

v0.10.0 已发布 — 升级前请阅读

两项破坏性授权变更,均需要运维人员采取操作: 通配符工具范围("tools": ["*"])不再授予 15 个具备写入能力的 工具;tokens.json 的权限必须为 0600,否则服务将拒绝启动。 新增的 token set-scope 命令可在不重新签发密钥的情况下更改令牌的范围,因此你可以在替换二进制文件前,先在正在运行的 0.9.x 服务上收窄范围。操作步骤请参见 升级到 v0.10。

安全性:现在,整个身份验证堆栈均不含 unsafe 代码 — rust-junosmcp-auth 使用共享的 mecmcp-auth crate, 以 zeroize 替代手动编写的密钥清零逻辑,并以 rustix 替代 libc::getuid。加载令牌存储时,格式错误的令牌条目也不再导致整个存储离线。

当前工具范围:默认提供 44 个工具(使用 --no-default-features 时仅提供 28 个 Junos 专用工具)。有关上述授权变更,请参见 v0.10.0 发布说明。

功能范围

v0.1(已发布)
  • 6 个工具:get_router_list、gather_device_facts、execute_junos_command、 get_junos_config、junos_config_diff、load_and_commit_config。
  • 仅支持 stdio 传输。
  • 兼容直接使用 devices.json(auth.type ∈ {password, ssh_key})。
  • 提供 Docker 镜像(distroless)和包含 systemd 单元的 LXC 发布压缩包。
v0.2(已发布)
  • streamable-http 传输(可选 rustls TLS)。
  • 按令牌设置路由器和工具范围的 bearer-token 身份验证。
  • 支持通过 SIGHUP 热重载令牌存储。
v0.2 后续版本:PFE + 批处理(已发布)
  • execute_junos_pfe_command — 针对明确指定的 FPC 目标执行单次 PFE shell 调用。
  • execute_junos_command_batch — 对 N 台路由器执行 M 条操作 CLI 命令;跨路由器并行执行;支持按命令设置超时,以及可选的整批超时。预检时会检查阻止列表和未知路由器;预检后遇错仍继续执行。
  • 在 _blocklist_defaults 和每台设备的 blocklist 下新增 pfe_commands 规则列表。该列表独立于 commands。
v0.2 后续更新:模板(已发布)
  • render_and_apply_j2_template — 使用 JSON vars_content 对象渲染 Jinja2 模板(内联 template_content)。支持单个路由器(router_name)或多个路由器(router_names)、试运行和完整提交。沿用与 load_and_commit_config 相同的阻止列表和格式门控。
  • Vars 必须是顶层 JSON 对象。自 v0.5.2 起,不再接受 YAML(RJMCP-SEC-002):MCP 输入可触及 serde_yml / libyml 公告链(RUSTSEC-2025-0067/-0068),因此移除了 YAML 分支。
  • 大小上限:template_content 和 vars_content 各自最多为 64 KiB。
  • 严格未定义模式:缺少变量时会报错并指出变量名,而不是渲染为空。
  • 自动检测格式:以 < 开头 → xml,包含任意 set / delete 行 → set,否则为 text。通过 config_format 覆盖。
  • 结果结构:每个路由器对应一行,包含 rendered_template、config_format,以及以下其中一项:diff(试运行)、commit_comment(应用模式下回显提供的注释 — rustez 不会返回服务端生成的提交 ID),或 error。
v0.2 后续更新:清单变更(已发布)
  • add_device — 将 Junos 设备添加到内存清单并持久化到 devices.json。采用原子写入(临时文件 + 重命名),保留 _blocklist_defaults、每台设备的 blocklist 和其他顶层字段。基于 SHA-256 的 TOCTOU 防护会拒绝与外部编辑操作发生竞争的调用。
  • reload_devices — 重新读取当前的 --device-mapping(不带参数),或切换到新的清单文件(file_name)。报告新增、移除或变更的设备名称。
  • 新 CLI 标志:--inventory-readonly(无条件拒绝这两个工具)、--allow-password-auth-add(允许 add_device 中使用 auth.type=password;不能与 --inventory-readonly 同时使用)。
  • SIGHUP 现在除了重新读取令牌存储外,也会重新读取清单。

已记录的注意事项: add_device 不会修改令牌存储。如果令牌具有 --routers 'edge-*',而你通过 add_device 添加 core-3,现有令牌将无法看到新路由器。在 add_device 后签发新令牌或轮换作用域。

v0.3(已发布)
  • NETCONF 会话池 — 使用 PooledDevice RAII 守卫和每个路由器的单槽会话池(空闲超时 300s、SSH 保活 30s、后台清理器)。消除顺序执行命令时的 SSH 握手开销。
  • 工具可靠性修复 — 为 get_junos_config 和 junos_config_diff 移除 XML 包装层;修正 show configuration | compare rollback N 命令;超时现在涵盖 SSH 连接和 NETCONF 握手(而不只是 CLI 执行)。
  • 批处理部分结果 — execute_junos_command_batch 遇到未知或无法访问的路由器时,会返回内联错误行,而不是中止整个批处理。违反阻止列表的情况仍会严格处理。
  • 批处理作用域违规不返回部分结果 — 如果请求中的任何路由器不在令牌作用域内,调用将被拒绝,并返回 HTTP 403 insufficient_scope,且不会执行任何路由器上的操作。这是有意为之,与上文无法访问的情况不同:设备宕机属于运行时故障,而指定调用者无权访问的设备属于授权失败;未经授权的请求不会有任何部分得到执行。请拆分请求或扩大令牌作用域。
  • 确认提交 — load_and_commit_config 新增 confirm_timeout_mins 参数,用于执行 commit confirmed N,并提供自动回滚安全保障。
  • crates.io 依赖 — rustez 从路径依赖切换为 crates.io 0.10.1;CI 不再需要检出同级仓库。
v0.4(已发布)
  • transfer_file — 通过幂等 SCP 将主机暂存文件推送到 Junos 设备上的 /var/tmp/<basename>(使用 scp -O,因为 Junos 会禁用 OpenSSH SFTP)。传输前检查 /var 的可用空间(local_size + 32 MiB 余量),验证 SHA-256,并在传输后重新验证校验和;若不匹配则删除文件。仅支持 SSH 密钥认证——拒绝使用密码认证的设备,并返回 [code=unsupported_auth]。
  • list_staged_files — 始终列出主机暂存目录;提供 router_name 时,还会列出设备上的 /var/tmp/。
  • 稳定的错误代码 — 每个传输失败都会带有 LLM 可读的 [code=...] Display 标签(bad_source_path、insufficient_disk、unsupported_auth、dest_exists_differs、scp_failed、connect_timeout、host_key_mismatch、host_key_revoked、verify_mismatch、outer_timeout、device_probe_failed)。
  • 新增 CLI 标志 — --staging-dir(默认值为 /var/lib/jmcp/staging)和 --known-hosts-file(默认值为 /etc/jmcp/known_hosts)。
  • 打包 — install.sh 会创建由 jmcp:jmcp 所有的新磁盘目录结构。详情请参阅下文的文件传输部分。
  • 工具数量:11 → 13。
v0.5(已发布)
  • upgrade_junos — 分两次调用(暂存后确认)升级 Junos 软件。按照 transfer_file 的语义上传软件包,运行 request system software add,然后重启。仅适用于独立运行的设备;若目标路由器存在会话池条目,则拒绝执行。
  • 工具数量:13 → 14。
v0.6(已发布)
  • fetch_file — 将文件从 <device>:/var/tmp/<basename> 下载到主机暂存目录。使用 SHA-256 验证;若本地副本已匹配,则幂等跳过;按路由器串行执行。与 transfer_file 相对应。
  • 工具数量:14 → 15。
v0.7(已发布)
  • commit_check_config — 验证候选配置(commit check)但不提交——加载、比较差异、检查,然后丢弃。绝不激活配置。使用独立令牌作用域(最小权限)。
  • discard_candidate — 丢弃未提交的候选更改(rollback 0),以恢复处于脏状态(“configuration database modified”)的候选配置。绝不更改正在运行的配置。使用独立令牌作用域(最小权限)。
  • 工具数量:15 → 17。
v0.8(已发布)
  • 统一的 Junos/SRX 服务 — 一个二进制文件、端点、进程、清单、设备管理器、租约管理器、认证接口和 SIGHUP 重载路径。默认构建提供 26 个工具;--no-default-features 保留仅支持 Junos 的 17 个工具接口。
  • 运行控制 — 按路由器限制并发数,按令牌限制会话数和 RPS,严格控制全局会话准入,提供有界 Prometheus 指标,并可选择原生 journald 审计分流。
  • 安全与打包 — 限定路由器列表结果的范围,跨进程租约保护破坏性操作,支持容器 SCP,使用非 root 镜像运行时、进程健康检查,并覆盖软件包升级场景。
  • 拆分服务迁移 — 升级时会移除 rust-srxmcp、其 systemd 单元和端口 30032,同时保留对支持包的支持。仅 v0.8.0 接受已弃用的 JMCP_SRX_* 别名。
v0.9(已发布)
  • rollback_config 工具 — 将 Junos 回滚归档(回滚 N,0–49) 加载到候选配置中,并预览(默认行为)或提交,支持确认提交。 工具数量:26 → 27(仅适用于 Junos 的工具:17 → 18)。
  • 正确的管道过滤 — | match / | except 在服务端应用; <command> RPC 会静默丢弃这些过滤条件,因此过滤后的配置查询 曾返回完整配置(导致审计出现未被察觉的假阴性)。
  • 更安全的配置动词 — junos_config_diff 接受 rollback 0 (候选配置与运行配置对比);commit_check_config 报告三种 outcome(valid / invalid / check_failed),因此不会将多 RE 集群 检查中无法得出结论的结果误读为无效配置;discard_candidate 可在不锁定的情况下恢复 已变更的候选配置。
  • 更清晰的诊断信息 — SRX 服务状态报告会将失败的健康检查 RPC 标记为 error, 而非 not_configured;路由器解析失败时会记录名称是未知还是超出作用范围(客户端响应不变)。
  • 安全性 — SSH 传输不再使用预发布版 RustCrypto(russh 0.62;-rc 加密 crate 从 13 个减少至 3 个)。
v0.10(已发布)
  • 通配符工具作用范围不包含写入工具(不兼容变更) — "tools": ["*"] 可访问所有只读工具,但不包含 15 个可写入工具;授予写入权限现在始终需要明确指定工具名称。 请参阅工具作用范围与写入工具。
  • tokens.json 的权限必须为 0600(不兼容变更) — 如果令牌文件可被组用户或其他用户访问, 服务将拒绝启动,并指出所有者 uid、权限模式、调用者 uid,以及应运行的 chmod 命令。
  • token set-scope — 无需重新签发密钥即可调整令牌作用范围,因此可在升级二进制文件前缩小作用范围, 而不会导致客户端中断。
  • 弹性令牌加载 — 作用范围名称现在在写入时验证,而非加载时验证,因此单个过时条目不会导致整个存储离线。
  • 安全性 — rust-junosmcp-auth 现在是共享 mecmcp-auth crate 的轻量级重新导出层,且不包含 unsafe;使用 zeroize 代替手动编写的机密清零代码,使用 rustix 代替 libc::getuid。工具数量不变(27 / 18)。
v0.11(已发布)
  • 新增四个只读 SRX 工具 — srx_list_policies(按 from-zone/to-zone 上下文 列出安全策略,包括全局策略和可选的每策略命中计数)、srx_resolve_address 和 srx_resolve_application (解析地址簿和应用程序/应用程序集,包括 junos-* 预定义默认值;支持递归解析嵌套集合,并 明确拒绝引用循环,绝不会陷入循环或静默返回不完整答案),以及 srx_list_nat_rules(分别列出源 NAT、目标 NAT 和静态 NAT 规则)。地址簿和应用程序解析通过手工构建、按子树过滤的 get-configuration RPC 从配置中获取,因为 rustez 的 call() 仅支持扁平键/值参数。策略和 NAT 规则中的名称 (地址、应用程序)会有意保持未解析——srx_resolve_address / srx_resolve_application 是单独的显式步骤。四个工具都会分页/限制结果数量,并显式提供 truncated 标志,而非静默截断。工具数量:37 → 41(SRX 工具:9 → 13;仅适用于 Junos 的构建不变, 仍为 27 / 18,因为这些工具受默认的 srx feature 控制)。
v0.12(未发布)
  • 默认启用提交确认(不兼容变更) — load_and_commit_config、 rollback_config(commit=true)、render_and_apply_j2_template 和 apply_junos_change_set 现在都会执行 commit confirmed,除非调用方通过 confirm_timeout_mins: 0 明确选择退出。此前默认执行的是普通的无条件提交;如果模型起草的变更导致管理访问中断,就没有机制让设备恢复。参见 确认提交。
  • --commit-confirm-default-mins(默认值为 10)用于设置服务器级时间窗口,供调用未指定 confirm_timeout_mins 时使用;单次调用的值仍会覆盖此设置。
  • confirm_commit — 新增的写入工具,用于为上述四种路径中的任意一种发送确认提交,取消待执行的自动回滚。
  • 选择退出会在审计事件中记录为 commit_confirmed=false。
  • 工具数量:43 → 44(仅 Junos 工具为 27 → 28)。

黑名单防护措施(v0.2)

devices.json 可以包含可选的 _blocklist_defaults 块,以及每个设备条目中可选的 blocklist 字段。规则使用简单的 glob(*、?)和 "deny" 或 "allow" 操作。优先采用最具体的匹配项;具体设备规则优先于顶层默认规则。示例参见 devices-template.json,完整设计参见 docs/superpowers/specs/2026-05-04-blocklist-guardrails-design.md。

pfe_commands 规则列表彼此独立:对 commands 的拒绝规则不会限制 execute_junos_pfe_command,反之亦然。可使用它来限制 PFE 输入(例如 set *),而不影响运维 CLI。

黑名单适用于 execute_junos_command 和 load_and_commit_config。 对于 load_and_commit_config,只要设备存在任何生效的配置规则,config_format 就必须为 set;在这种情况下,text 和 xml 负载会在预检阶段被拒绝。

兼容性说明: 使用 _blocklist_defaults 或设备级 blocklist 的文件,与 Juniper/junos-mcp-server 的清单格式不兼容。未包含这些字段的文件仍可直接兼容。

execute_junos_command 授权模式:允许列表(默认)与阻止列表

_blocklist_defaults(以及今后仅此键——请参见下文的设备级限制)可以设置 mode 为 "allowlist" 或 "blocklist"。此设置适用于 execute_junos_command、execute_junos_command_batch、execute_junos_pfe_command,以及 get_junos_config(它会通过同一组 commands 允许列表/阻止列表运行 show configuration [path] [| display <format>]——请参见下文的“配置输出格式和加载模式”);load_and_commit_config 使用的 config 域无论 mode 如何,始终采用故障开放的阻止列表。

  • allowlist(故障关闭,新配置的默认模式)——除非命令匹配 allow 中按空白字符拆分的字面前缀,否则一律拒绝(加载时会拒绝通配符,而不只是忽略)。命令中 | 后的每个阶段都必须单独匹配 allowed_pipes 中的前缀,否则拒绝整个命令;在匹配任何前缀之前,会直接拒绝被禁止的 shell 元字符(;、重定向符、反引号、换行符)。allow/allowed_pipes 的合并方式与当前 commands 拒绝规则相同:_blocklist_defaults ∪ 设备自身的列表。设备级 allow 中的新增项不会泄漏到其他设备——每台设备都有独立编译的允许列表策略。

    execute_junos_pfe_command 由其自身独立的一组键控制:pfe_allow/pfe_allowed_pipes。它们的合并方式与 allow/allowed_pipes 相同(默认值 ∪ 设备级配置),但 allow 中的条目不会允许任何 PFE 命令,反之亦然——这两个域绝不共享状态。若配置设置了 allow,但未设置 pfe_allow,则在允许列表模式下仍会拒绝所有 PFE 命令——未配置的域默认故障关闭,这是预期行为,并非错误;如果需要使用 execute_junos_pfe_command,请添加 pfe_allow 列表。

  • blocklist(故障开放,旧版模式)——沿用 MEC-93 之前的行为:仅当命令匹配 commands(或 pfe_commands)拒绝通配规则时才会拒绝;其他命令一律允许。

允许的字符(MEC-1337)。 在允许列表模式下,命令只能使用可打印的 ASCII 字符,并以字面 ASCII 空格(U+0020)作为令牌分隔符。任何其他字符都会被直接拒绝(forbidden_metachar),处理方式与现有的 ;/重定向符/反引号/换行符检查相同。

迁移: 含有 commands/pfe_commands 拒绝规则但没有 mode 键的 devices.json,会以 blocklist 模式加载,并在启动时记录一条 WARN,说明阻止列表模式是故障开放的,并指向本节。完全不含 _blocklist_defaults 的文件,或新生成的示例配置,会以 allowlist 模式加载。mode 仅对 _blocklist_defaults 有效——在设备级 blocklist 上设置该项会导致加载时报错,因为底层策略引擎会为整个文件选择一种命令模式,设备级覆盖会被静默忽略。

每次拒绝(无论是允许列表还是阻止列表模式)都会通过现有审计路径写入审计记录,并使用稳定的原因代码标记:not_allowlisted、pipe_not_allowlisted、forbidden_metachar 或旧版代码 blocked。

最简只读入门允许列表:

"_blocklist_defaults": {
    "mode": "allowlist",
    "allow": [
        "show version",
        "show interfaces",
        "show route",
        "show security policies",
        "show chassis"
    ],
    "pfe_allow": [
        "show cos"
    ]
}

配置输出格式和加载模式

get_junos_config 可选接受 format:text(默认值,保持不变)、 set、xml 或 json。每种格式都通过对应的 Junos | display <format> CLI 修饰符在设备端渲染,因此输出会经过与当前 text 相同的策略检查、阻止列表和输出上限管道——新格式没有单独的代码路径。

{ "router_name": "core-1", "config_path": "system services", "format": "set" }

在 mode: allowlist 下,get_junos_config 由 commands 允许列表管控,而非单独的规则。 渲染后的命令为 show configuration [config_path] [| display set|xml|json],检查方式与其他 execute_junos_command 输入相同:show configuration 必须是 allow 中的前缀;如果请求的 format 不是默认的 text,那么 display set/display xml/display json 必须是 allowed_pipes 中的前缀。 devices-template.json 中的起始允许列表不包含 show configuration,因此照搬该配置会完全拒绝 get_junos_config (默认拒绝,因此安全,但很容易忽略)。将 "show configuration" 添加到 allow(并将所需的 display 变体添加到 allowed_pipes)即可启用此工具——但请注意,这会向模型公开完整的运行配置,其中包括经过哈希处理的密钥。

load_and_commit_config、render_and_apply_j2_template,以及 create_junos_change_set 中每个操作的 payload 均可选接受 mode:merge(默认值,保持不变)、replace 或 override。

override 会替换整个候选配置——这是此服务提供的影响范围最大的操作。 load_and_commit_config 和 render_and_apply_j2_template 会在同一次调用中直接提交,不经过第二位主体审核,因此会直接拒绝 override。该模式只能通过变更集流程使用(create_junos_change_set → approve_junos_change_set → apply_junos_change_set),该流程要求第二位主体先批准计划,之后才会提交任何内容。

config_format=set(即 configuration-set,也就是 set/delete 命令列表)在 Junos 中没有线级的 override 操作——在发送任何 RPC 之前,所有路径都会拒绝此组合,包括变更集。

已确认提交(v0.3 起提供,v0.12 起默认启用)

load_and_commit_config、rollback_config(使用 commit=true 时)、 render_and_apply_j2_template 和 apply_junos_change_set 默认均通过 Junos commit confirmed 提交——如果在时间窗口内没有确认更改,路由器会自动回滚,因此即使错误推送切断了管理访问,也会自行恢复,无需派人现场处理。

默认时间窗口由服务的 --commit-confirm-default-mins 标志设置 (默认为 10,验证方式与每次调用的参数相同)。每次调用的 confirm_timeout_mins 会覆盖该次提交的默认值:

{
  "router_name": "core-1",
  "config_text": "set interfaces ge-0/0/0 description test",
  "confirm_timeout_mins": 10,
  "commit_comment": "safe change with rollback window"
}

响应:

{
  "success": true,
  "diff": "[edit interfaces ge-0/0/0]\n+   description test;",
  "confirmed": true,
  "rollback_in_minutes": 10,
  "rollback_deadline_unix": 1780000600,
  "message": "Commit confirmed: auto-rollback in 10 minutes unless confirmed. Send another commit to confirm."
}

apply_junos_change_set 的状态响应会报告相同的 rollback_deadline_unix。

若要为单次调用停用已确认提交,请传入 confirm_timeout_mins: 0;这会执行普通的无条件提交,并在审计事件中记录为 commit_confirmed=false,以便事后追踪此例外。

若要确认待处理的时间窗口(取消计划的自动回滚),请使用设备名称调用 confirm_commit 工具——它会发送确认提交,方式与变更集流程中的 confirm_junos_change_set 相同。再次发送 load_and_commit_config(或 rollback_config / render_and_apply_j2_template)也会确认,因为 Junos 会将对候选配置的任何提交都视为确认,不论是由哪个工具发起。在已确认提交的时间窗口内,upgrade_junos 会拒绝继续(commit_confirmed_active),以免重启设备后,新映像运行期间配置仍可能回滚。

文件传输(transfer_file / fetch_file / list_staged_files)

transfer_file 会通过 SSH exec 通道,使用 mecmcp-scp 内置的 SCP1 客户端,将主机暂存的文件推送到 Junos 设备上的 /var/tmp/<basename>。这是与 OpenSSH 强制使用的 scp -O 相同的线路协议,因为 Junos 会禁用基于 SSH 的 SFTP。不会启动外部 scp 进程(#212)。该操作基于 SHA-256 实现幂等:如果远程文件已存在且摘要匹配,调用将返回 status: "skipped"。如果摘要不同,传入 force: true 可覆盖文件。

fetch_file 是镜像操作:它使用相同的内置 SCP1 路径,将 Junos 设备上的 /var/tmp/<basename> 下载到主机暂存目录。该操作基于 SHA-256 实现幂等——如果本地文件已存在且摘要匹配,调用将返回 status: "skipped"。与 transfer_file 一样,该操作也会对每台路由器进行串行处理,并在传输后重新验证 SHA-256。

**身份验证:**仅支持 SSH 密钥。对于 auth.type = "password" 的设备,系统会以 [code=unsupported_auth] 拒绝请求。请为设备添加 SSH 密钥,并在 devices.json 中通过 auth.private_key_path 指定密钥路径。

磁盘上的文件与目录:

路径用途默认权限所有者
/var/lib/jmcp/staging/主机侧暂存目录,用于存放待传输的文件0750jmcp:jmcp
/etc/jmcp/known_hosts每次推送时检查的 SSH known_hosts 文件0644jmcp:jmcp
/var/lib/jmcp/device-leasesJunos/SRX 破坏性操作共用的锁目录0700jmcp:jmcp

启动时可通过 --staging-dir <path>、--known-hosts-file <path> 和 --device-lease-dir <path> 指定替代路径。Junos 和 SRX 服务必须使用相同的设备租约目录。

**主机密钥策略(v0.5.2+):**默认启用严格的主机密钥检查——拒绝未知设备的主机密钥。首次调用 transfer_file / upgrade_junos 前,known_hosts 文件必须已存在,否则工具会报错 [code=known_hosts_missing]。可使用随附的辅助工具预先填充该文件:

scripts/scan-known-hosts.sh --inventory /etc/jmcp/devices.json \
                            --known-hosts /etc/jmcp/known_hosts

在实验室或首次连接时,可传入 --ssh-accept-new-host-keys,以回退到 OpenSSH 的 accept-new(TOFU)模式:首次连接时,未知主机会被固定记录到 known_hosts 中;之后如果主机提供了不同的密钥,仍会被拒绝。此策略同样适用于 transfer_file / upgrade_junos(scp)和 NETCONF SSH。

**--ssh-insecure-accept-any-host-key(仅限实验室):**完全跳过 NETCONF SSH 和 scp(transfer_file / upgrade_junos)的主机密钥验证——不持久化 known_hosts,不检测密钥不匹配,也无法防范中间人攻击。该选项不能与 --ssh-accept-new-host-keys 同时使用。启动时会醒目地记录此设置,并将其记为审计事件。切勿在生产设备上使用;请改用可安全实现 TOFU 语义的 --ssh-accept-new-host-keys。

list_staged_files 会返回主机暂存目录中的内容。如果指定了 router_name,该工具还会在设备上运行 file list /var/tmp/ detail,并将结果包含在 device_files 下。

源路径安全:source_path 必须仅为基本文件名(不得包含 /、\、..,不得以点开头,且长度不得超过 255 字节);它会相对于 --staging-dir 解析,且不会逃逸出该目录。

**传输前检查:**传输前,transfer_file 会运行 show system storage no-forwarding;如果 /var 上的可用空间小于 local_size + 32 MiB,则会拒绝推送。

**传输后验证:**除非传入 verify: false,否则会通过 file checksum sha-256 /var/tmp/<basename> 重新计算设备端校验和;如果不匹配,则会删除该文件。

长时间运行的操作命令

每个 MCP 工具都提供一个按调用设置的 timeout 参数(默认 360 s)。这是操作时长唯一对用户可见的限制;底层 rustez::Device 在连接时会配置 1 小时的内部 RPC 超时,因此,确实需要运行数分钟的命令(request system software add、request support information、request system snapshot 等)不会被悄然截断。

如果需要运行超过 1 小时的操作,请将其拆分为多个阶段,或在设备上以即发即弃的方式启动任务,并单独轮询完成状态。

注意: 如果长时间运行的 RPC 后设备重启,NETCONF 会话自然会断开。会话池会在下次调用时正常重新连接。

安全警告

此服务允许大模型在你的 Junos 设备上运行命令和推送配置更改。部署前请阅读 Juniper/junos-mcp-server 的安全声明。同样的警告也适用。

  • 优先使用 SSH 密钥认证,而不是密码。
  • 允许运行提交工具前,请先审核配置。
  • 限制对 MCP 服务的网络访问。
  • 不要部署到不可信的网络中。
  • 将 devices.json 的权限设为 0600——其中包含 SSH 凭据。
  • get_junos_config、junos_config_diff 以及其他会返回设备配置或命令输出的工具,会在响应发送给调用方之前对内容进行脱敏:与已知机密模式匹配的值(包括 Junos ## SECRET-DATA 值($9$... 格式的字符串,这类字符串使用 Juniper 的专有对称密码进行可逆加密,并非哈希——持有设备主密钥或密码短语的任何人都可以还原明文)、IKE 预共享密钥、RADIUS/TACACS 机密信息和 SNMP 社区字符串)会被替换为标记,同时保留结构、主机名和非机密值。这是尽力而为的机制(包含拒绝列表和基于值形态的兜底检测),并非万无一失;仍应将此工具的使用范围限制在可信令牌内。
  • reload_devices 要求 file_name 是解析后位于原始 --device-mapping 目录内的相对路径(自 v0.5.2 起)。绝对路径、.. 路径遍历,以及指向清单目录外部的符号链接,都会被拒绝。
  • 文本输入字段(command、config_text、template_content、pfe_command)的上限为 1 MB。批处理列表最多包含 100 台路由器和 50 条命令。

--lab-mode

--lab-mode 可豁免变更集的第二负责人要求,适用于双人控制难以落实的单人操作实验室环境。此选项默认关闭,管理生产设备的服务器不得启用。

它会和不会改变的内容:

  • 创建变更集时会自动批准。没有单独的批准步骤,流程仍为 plan → apply,与生产环境完全相同。
  • 规划、计划摘要、漂移检测和应用时重新验证仍会运行。实验室模式移除的是第二位审核者,而非变更记录。
  • 不会伪造批准者。 被豁免的变更集会记录 approver: null 和 approval_waiver: "lab-mode"。它在加密学上可与真实的双人批准区分开来,事后也无法重新标记——这点很重要,因为日后可能需要证明哪些变更确实实现了职责分离。
  • 启用时,服务器会在启动时发出醒目的警告。

如果你想在不豁免控制要求的情况下单人测试写入功能,可以创建两个名称不同的令牌,一个用于规划,另一个用于批准,但这种做法的保障能力不如表面看起来那么强:两个令牌都属于同一个人,因此所谓的“双人”审核实际上只是同一位操作员换个名字批准自己的计划。负责人是令牌名称,系统会拒绝自我批准,因此这种做法确实如实测试了 plan→approve→apply API 流程,但并不构成真正的第二位审核者。

mecmcp 0.27.0 添加了经过验证的人工批准者流程(MEC-994/MEC-995),通过将批准者的身份绑定到新鲜的 IdP 登录(而非令牌名称)来弥补这一缺口:

  • 创建时将所有者令牌绑定到 IdP 身份: token add --oidc-issuer https://idp.example.com --oidc-subject [email protected] ...。
  • 使用 --oidc-issuer、--oidc-audience 和 --require-verified-approver 启动服务器(还需要 --approval-digest-key-file,严格模式要求此选项,以确保经过验证的批准者字段具有防篡改能力)。
  • 使用 mecmcp-approve 批准。该工具会执行真实的 OIDC 登录(默认使用 PKCE),并将生成的断言附加到 Mecmcp-Approver-Assertion 标头中: mecmcp-approve --server-url https://junos01.example:8443/mcp --approve-tool approve_junos_change_set --oidc-issuer https://idp.example.com --oidc-client-id mecmcp-approve --arg change_set_id=... --arg device=... --arg expected_digest=...。
  • 在严格模式下,如果经过验证的主体与已绑定的所有者主体相同,协调器会拒绝批准——无论同一个人持有多少个不同名称的令牌,都无法同时满足双人规则的双方要求。

如果没有 IdP,可使用上述双令牌变通方案对 plan→approve→apply 流程进行纯功能测试,但要明确它的实际意义:这是一个操作员在测试 API 流程,并非真正的批准。

已验证的审批者:设置与失败行为

每次构建都会编译此功能(不是 Cargo 功能),且只有设置 --oidc-issuer 后才会启用。设置流程遵循 mecmcp 的 入门指南; 此服务唯一不同之处是工具名称: 设置 --oidc-issuer,但不启用 --require-verified-approver 时,启动日志会警告并统计没有 oidc_subject 绑定的有效 human 令牌数量。

# 1. Bind the approver's token to their IdP identity. Packaged installs keep the
#    store at /var/lib/jmcp/tokens.json. If yours is still at /etc/jmcp/tokens.json
#    (pre-0.22), migrate it first as install.sh prints; never create a second one.
sudo -u jmcp rust-junosmcp token add --tokens-file /var/lib/jmcp/tokens.json --name alice \
  --actor-type human --tools approve_junos_change_set --devices '*' \
  --oidc-issuer https://idp.example.com/ --oidc-subject [email protected]

# 2. Create the approval digest key strict mode requires: at least 32 random
#    bytes, mode 0600, owned by the service user (`jmcp` on a package install).
#    Written as jmcp, never as root (/etc/jmcp is service-owned on packaged installs).
#    `set -C` keeps an existing key, since replacing it orphans pending approvals;
#    if one exists, confirm it is 0600 jmcp with `stat -c '%a %U' <path>`.
sudo -u jmcp sh -c 'umask 077; set -C; head -c 48 /dev/urandom | base64 > /etc/jmcp/approval-digest.key'

# 3. Run on the HTTP transport with the token store (assertions apply to
#    bearer-token callers only) and the verifier configured.
sudo -u jmcp rust-junosmcp --device-mapping /etc/jmcp/devices.json \
  --transport streamable-http -H 127.0.0.1 -p 8765 \
  --tokens-file /var/lib/jmcp/tokens.json \
  --oidc-issuer https://idp.example.com/ --oidc-audience YOUR_AUDIENCE \
  --approval-digest-key-file /etc/jmcp/approval-digest.key \
  --approver-replay-store /var/lib/jmcp/approver-replay.log \
  --require-verified-approver

--approver-replay-store 会在重启后保留已使用的断言 jti(由服务用户以 0600 权限创建,因此 /var/lib/jmcp 是合适的位置;如果没有此选项,重启前刚使用过的断言在过期前仍可被重放一次)。它需要 --oidc-issuer。

  • 启用 --require-verified-approver 后,没有有效断言的审批会被拒绝,没有绑定主题的所有者令牌提交的提案也会被拒绝。
  • 仅在审批调用时发送 Mecmcp-Approver-Assertion。 传输层会对任何携带该标头的请求进行验证,无论工具为何:无效断言(主题与令牌绑定的主题不同、已过期、已重放,或无法访问 IdP 的密钥)会收到 401;如果在较早的读取或提案请求中使用了有效断言,其单次使用的 jti 就会被消耗,之后的审批会因重放而被拒绝。不带此标头的请求不受验证器影响。
  • 未设置 --oidc-issuer 的服务会拒绝任何携带此标头的请求,并返回 400。
已验证的提案者

--require-verified-proposer 与 --require-verified-approver 配合使用,使双人规则比较两个经 IdP 验证的主题,而不是比较操作员在 token add 中手动填写的主题。它需要 --oidc-issuer 和 --oidc-audience。启用后,create_junos_change_set 必须携带新的 Mecmcp-Proposer-Assertion 标头,且其主题必须与提案令牌绑定的 oidc_subject 相同;通过 OAuth 身份验证的调用方无需断言也会被视为已验证。提案令牌既不需要 --actor-type human,也不需要审批者角色。变更集会记录其所有者已通过验证;审批、豁免和应用操作会拒绝未通过验证的变更集,因此在启用此标志之前提交的变更集也不能绕过此要求。

仅在 create_junos_change_set 上发送 Mecmcp-Proposer-Assertion,且绝不要在此处发送 Mecmcp-Approver-Assertion:两个标头彼此独立,但共用同一组单次使用的 jti,因此在任何其他请求中消耗的断言都不能重复使用。默认关闭。请注意,任何已验证的提案者(包括所有 OAuth 调用方)都会使服务持久化变更集状态版本 9,而 v2.3.0 无法读取该版本:升级前请先备份状态文件,因为回滚时需要恢复该文件。

启用

将 --lab-mode 添加到服务单元中。在软件包安装时,请使用 systemd drop-in 文件,而不要编辑随软件包提供的单元文件,以免升级时意外移除此选项:

sudo systemctl edit rust-junosmcp

替换 ExecStart 时必须完整重述其内容,因此请复制随软件包提供的命令并追加此标志,不要写成较短的命令。省略其他参数会在启用实验室模式时,连带关闭结构化审计或 HMAC 脱敏:

[Service]
# Clear the shipped ExecStart before replacing it; systemd appends otherwise.
ExecStart=
ExecStart=/usr/local/bin/rust-junosmcp \
    --device-mapping /etc/jmcp/devices.json \
    --transport streamable-http \
    --host 127.0.0.1 \
    --port 30030 \
    --tokens-file /etc/jmcp/tokens.json \
    --audit-format json \
    --audit-journald \
    --audit-redact devices=hmac \
    --audit-hmac-key-file /etc/jmcp/audit-hmac.key \
    --lab-mode

应用前请对照 packaging/systemd/rust-junosmcp.service 检查——随软件包提供的参数是权威依据,而此代码片段只是副本,可能会过时。

sudo systemctl daemon-reload && sudo systemctl restart rust-junosmcp

确认设置已生效。启动警告使用 target: "audit",因此请在具备足够权限以查看系统单元的情况下,在日志中搜索实验室模式文本:

sudo journalctl -u rust-junosmcp -b | grep -i "lab mode"

预期输出包括:

lab mode enabled: change sets are approved on creation with no second principal. Records carry approval_waiver=lab-mode. Do not run this against production devices.

没有输出表示该功能未启用。由于权限不足,非特权用户运行的 journalctl 也可能在此处不显示任何内容,而非标志未设置;因此命令使用了 sudo。

--allow-direct-commit

有四个工具根本不会创建变更集:load_and_commit_config 无条件如此;render_and_apply_j2_template(实际执行应用时)、rollback_config(commit=true 时)和 upgrade_junos(confirm=true 时)则是在只读预览/预检步骤之后如此。这些工具在一次调用中暂存、验证并提交设备变更,因此从机制上说不存在第二位负责人审核——没有变更集可供第二位负责人批准。默认关闭:若不启用此标志,这四个工具都会在触及设备之前被拒绝;stdio 下也与 HTTP 下完全一样(stdio 完全不携带调用者上下文,因此不能比经过身份验证的会话受到更宽松的对待)。

残余风险。 --allow-direct-commit 是一个逃生口,并非解决方案。设置此标志的操作员已决定,对于这些特定工具,在没有独立审核的情况下运行,对此部署而言是可接受的风险。此决定会:

  • 在启动时醒目记录。 每次进程在启用此标志的情况下启动,都会输出一条 SECURITY: 警告,明确说明风险。
  • 在每次调用时审计。 每次直接提交调用都会在其审计记录中携带 direct_commit_allowed=true——拒绝调用也会被审计,记录为 authorization=denied reason=direct_commit_disabled。

它不会增加第二位负责人的审核,只会让无审核运行这件事显而易见。只要工作流支持,就应优先使用变更集流程(create_junos_change_set → approve_junos_change_set → apply_junos_change_set),并将此标志留给无法使用该流程的特定工具。

启用

与上文的 --lab-mode 相同:通过 systemd drop-in 将 --allow-direct-commit 添加到服务单元,并完整复制随附的 ExecStart,不要编写更短的命令。

确认设置已生效。与 --lab-mode 横幅不同,此横幅有意不标记为 target: "audit"——该流具有固定的单次调用架构,而启动横幅不包含其中任何字段——因此请改为 grep 普通消息文本:

sudo journalctl -u rust-junosmcp -b | grep -i "allow-direct-commit is enabled"

没有输出表示此标志处于关闭状态。

审计日志记录

rust-junosmcp 会为每次 Junos 和 SRX 工具调用生成结构化审计事件。每个事件都会记录调用者、工具、目标路由器、授权决定、结果和耗时。完整架构和转发指南请参阅 docs/AUDIT.md。

标志环境变量默认值说明
--audit-formatJMCP_AUDIT_FORMATtext输出格式:text 或 json。
--audit-log-fileJMCP_AUDIT_LOG_FILE(无)可选文件路径;除 stderr 外,还会将 JSON 事件追加到该文件。
--audit-journaldJMCP_AUDIT_JOURNALDfalse可选的原生 journald 结构化审计字段分流;明确启用但不可用时,启动会失败。

快速开始(本地)

git clone https://github.com/mechubsec/rustjunosmcp.git
cd RustJunosMCP

# Build the default 44-tool Junos/SRX server with TLS.
cargo build --release

# Optional: build the 28-tool Junos-only server without TLS.
cargo build --release --no-default-features

# Optional: build Junos-only with TLS.
cargo build --release --no-default-features --features tls

# Configure devices.
cp devices-template.json devices.json
$EDITOR devices.json   # set ip / username / auth

# Run as MCP stdio server.
./target/release/rust-junosmcp -f devices.json

每台设备都可以带有一个可选的 attributes 对象,仅供静态令牌上的 device_selectors(参见静态令牌上的设备选择器)和 OAuth 范围映射使用(参见 OAuth 2.1 资源服务器模式(选择启用的构建)):

"srx-lab-1": {
  "ip": "10.0.0.1", "username": "admin", "auth": {"type": "password", "password": "..."},
  "attributes": {"site": "lab-1", "vendor": "junos", "tags": ["pci", "edge"]}
}

site 和 vendor 是单个值,tags 是列表。值长度为 1-64 个字符,且字符必须来自 [A-Za-z0-9._-]。没有 attributes 的设备不匹配任何选择器。现有的 devices.json 文件可原样加载:如果 attributes 块不是有效的选择器元数据(包含未知键、值无效或不是对象),则加载时会针对每台设备发出一条警告,且该设备不匹配任何选择器。重命名该键即可消除此警告。

Claude Desktop 配置

一个注册即可公开内置二进制文件中启用的所有工具(默认 srx 功能下为 44 个,仅 Junos 构建下为 28 个):

{
  "mcpServers": {
    "junos": {
      "command": "/path/to/rust-junosmcp",
      "args": ["-f", "/path/to/devices.json"]
    }
  }
}

examples/ 中提供了可直接编辑的 Claude Desktop (claude_desktop_config.json)和 VS Code 配置(vscode-mcp.json,用于 .vscode/mcp.json:stdio,以及令牌由用户输入而非存储的可流式传输 HTTP)。两者都以你自己的用户身份运行,因此会传入每用户专属的 --device-lease-dir、--state-file、--known-hosts-file、--staging-dir 和 --support-bundle-staging-dir 路径(/var/lib/jmcp 和 /etc/jmcp 下的默认路径属于服务账户;仅 Junos 构建中应省略最后一个标志), 且 devices.json 的权限必须为 0600。在 args 中添加 "--tool-mode", "compact",即可仅列出 execute 和 get_router_list。每个状态文件注册一个 stdio 实例:同一文件上的第二个进程会退出。

Docker

使用 Docker 运行

适用于目录的 stdio 调用会用 --transport stdio 替换镜像的 HTTP CMD。ENTRYPOINT 已提供清单、状态、known-hosts、租约和令牌文件路径,因此此处不要重复这些标志。 状态挂载是绑定挂载,因此初始时归主机用户所有;首次启动前,它和其中的 tokens.json 都必须归 UID/GID 65532:65532 所有,并将 tokens.json 的权限设为 0600:

mkdir -p jmcp-state
cat > jmcp-state/tokens.json <<'EOF'
{
  "version": 1,
  "tokens": [
    {
      "name": "catalog",
      "digest": "sha256:REPLACE_WITH_TOKEN_ADD_OUTPUT",
      "devices": ["r1"],
      "tools": ["gather_device_facts"],
      "created_at": "2026-01-01T00:00:00Z"
    }
  ]
}
EOF
sudo chown -R 65532:65532 jmcp-state
sudo chmod 0700 jmcp-state
sudo chmod 0600 jmcp-state/tokens.json
sudo chown 65532:65532 devices.json
sudo chmod 0600 devices.json

docker run --rm -i \
  -v "$PWD/devices.json:/etc/jmcp/devices.json:ro" \
  -v "$PWD/keys:/etc/jmcp/keys:ro" \
  -v "$PWD/jmcp-state:/var/lib/jmcp" \
  ghcr.io/mechubsec/rustjunosmcp:latest \
  --transport stdio

在 rootless Docker 或启用了 userns-remap 的主机上,容器内的 UID 65532 会映射到不同的主机 UID;请将挂载目录的所有权更改为实际映射的 UID,而不是直接使用 65532。

例如,devices.json 使用服务端的清单格式(使用前请替换占位密钥):

{
  "r1": {
    "ip": "192.0.2.10",
    "port": 22,
    "username": "netconf-user",
    "auth": {
      "type": "password",
      "password": "replace-with-device-password"
    }
  }
}

此调用会关闭 HTTP 和 TLS,因为替换镜像 CMD 也会移除其 HTTP 绑定和 TLS 相关标志。

服务端默认使用端口 22,通过设备常规的 SSH 服务传输 NETCONF(set system services ssh)。如果启用专用 NETCONF 监听器 set system services netconf ssh(默认端口为 830)并希望使用它,请将 port 显式设为 830。

关于将双人模式和实验室模式组合以容器运行的说明,包括会导致允许列表拒绝所有请求并返回 421 的已发布端口与内部端口陷阱,详见 docs/HOW-TO-SETUP-DOCKER.md。

每次发布版本标签时,都会将预构建镜像发布到 GHCR。该软件包是公开的——无需 docker login。运行时采用 distroless——不包含 shell、apt 或 OpenSSH 客户端(#201)——并以数字 UID/GID 65532:65532 运行。transfer_file/fetch_file 不再启动 scp;它们通过 SSH exec 通道使用 mecmcp-scp 原生实现 SCP1 线协议(#212)。

准备只读的配置/密钥挂载和一个持久化的可写状态目录。devices.json 中的私钥路径必须使用 /etc/jmcp/keys 下的容器内路径。

# Pull the prebuilt image (tags: latest, 0.25, 0.26, 0.27).
docker pull ghcr.io/mechubsec/rustjunosmcp:latest

# Prepare host paths. Review scanned host-key fingerprints against a trusted
# source before starting the server in strict mode.
mkdir -p keys jmcp-state/staging jmcp-state/device-leases
touch jmcp-state/known_hosts
./scripts/scan-known-hosts.sh \
  --inventory "$PWD/devices.json" \
  --known-hosts "$PWD/jmcp-state/known_hosts" \
  --replace

# The image's non-root process must own its writable state and be able to read
# the inventory and private keys. Keep all three private from other host users.
sudo chown -R 65532:65532 devices.json keys jmcp-state
sudo chmod 0600 devices.json keys/* jmcp-state/known_hosts
sudo chmod 0700 keys jmcp-state jmcp-state/device-leases
sudo chmod 0750 jmcp-state/staging

# Run with configuration/keys read-only and state persistent + writable.
docker run --rm -i \
  -v "$PWD/devices.json:/etc/jmcp/devices.json:ro" \
  -v "$PWD/keys:/etc/jmcp/keys:ro" \
  -v "$PWD/jmcp-state:/var/lib/jmcp" \
  ghcr.io/mechubsec/rustjunosmcp:latest

验证镜像签名:Release image 工作流推送的每个镜像都会通过 GitHub Actions OIDC 使用 cosign 进行无密钥签名——不存在任何密钥对。验证会将签名身份固定为该确切工作流,因此,来自其他任何地方的签名(分支仓库、不同仓库、本地构建)都会验证失败:

cosign verify \
  --certificate-identity-regexp '^https://github\.com/mechubsec/rustjunosmcp/\.github/workflows/release-image\.yml@refs/(tags/v[0-9]+\.[0-9]+\.[0-9]+|heads/main)$' \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/mechubsec/rustjunosmcp:latest

这是一个正则表达式,而不是精确的 --certificate-identity,因为 GitHub 会将触发运行的 ref 写入证书,而此工作流有两个合法触发条件,各自对应不同的 ref:普通标签推送携带 @refs/tags/vX.Y.Z,指向该版本自己的标签——每个版本都不同,包括 :latest 标签,因为它会在每次发布时重新推送并重新签名;通过 workflow_dispatch 进行的补发(见上面的 ref 输入)通常携带 @refs/heads/main,也就是触发运行的分支。拉取请求和对其他任何分支的推送都无法触发此工作流,因此不可能出现其他身份。如果你要验证某个特定发布版本,而不是“此工作流构建的某个发布版本”,请固定到确切标签,而不是版本范围。每次签名还会在 Rekor 透明日志中创建一个公开条目—— 这是预期行为,并不会泄露镜像推送本身尚未公开的任何信息。

2026-09-29 之前发布的镜像由 github.com/fastrevmd-lab/RustJunosMCP 下的工作流签名,因此验证较早的标签时需要使用该身份。

**验证 SBOM 证明:**发布时,Rust 依赖图(不包括镜像的 distroless 运行时基础镜像)的 CycloneDX SBOM 会附加到 GitHub 发布版本中,也会作为 in-toto 证明推送到镜像上,并以与上述相同的无密钥方式签名。此证明由本仓库 release-sbom.yml 中的 sbom: 作业签名;从 v0.27.5 之后的第一个发布版本开始,该作业会将实际的证明工作委托给 mecmcp 的可复用工作流 reusable-attest-release-sbom.yml,因此 OIDC 证书主体是该工作流的路径,并固定到 release-sbom.yml 的 uses: 行当前引用的确切提交 SHA——而不是本仓库自己的工作流文件,也不是分支 ref。每当 release-sbom.yml 重新固定到较新的 mecmcp SHA 或标签时,此固定值都会变化,因此不要认为本 README 中的 SHA 永远准确;请检查 .github/workflows/release-sbom.yml 中的 uses: 行,确认当前固定值。

由于该可复用工作流位于公开仓库中,任何 GitHub 仓库都可以调用它,并获得具有相同身份的证书,因此仅凭身份无法证明此证明来自本仓库的发布版本。--certificate-github-workflow-repository 和 --certificate-github-workflow-trigger 可以弥补这一点:它们会检查证书中的调用仓库和触发事件字段,这些字段必须分别为 mechubsec/rustjunosmcp 和 release。不要删除它们。此身份与下方发布压缩包签名的身份相同,不同于上方镜像签名的 release-image.yml 身份,因为这是一个在镜像推送完成后才运行的独立作业:

cosign verify-attestation --type cyclonedx \
  --certificate-identity-regexp '^https://github\.com/mechubsec/mecmcp/\.github/workflows/reusable-attest-release-sbom\.yml@[0-9a-f]{40}$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-github-workflow-repository mechubsec/rustjunosmcp \
  --certificate-github-workflow-trigger release \
  ghcr.io/mechubsec/rustjunosmcp:<version>

此处对 SHA 使用正则表达式(而不是精确的 --certificate-identity),因为 dependabot 或维护者可能会将 release-sbom.yml 重新固定到较新的 mecmcp 提交;这只固定路径,不会固定到某个特定提交。对于 v0.27.5 及之前的发布版本,证明则由本仓库自己的 release-sbom.yml 工作流身份直接签名:

cosign verify-attestation --type cyclonedx \
  --certificate-identity-regexp '^https://github\.com/mechubsec/rustjunosmcp/\.github/workflows/release-sbom\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/mechubsec/rustjunosmcp:<version>

状态挂载卷存放暂存的上传/下载文件、共享的破坏性操作租约以及 known_hosts。服务器运行时请勿删除其租约文件。默认启用严格的主机密钥检查。仅限隔离实验室使用时,可在 docker run 命令中追加 --ssh-accept-new-host-keys;这会让服务器将首次发现的密钥添加到可写状态文件中,但不会通过带外方式验证首次连接。

**Apple Silicon(M 系列):**镜像仅为 linux/amd64 构建,因此会在 Apple Silicon 上通过模拟运行。虽然可以正常运行,但如果遇到平台不匹配警告,请在 pull 和 run 命令中都添加 --platform linux/amd64。

建议改为在本地构建:

docker build -t rust-junosmcp:local .

docker run --rm -i \
  -v "$PWD/devices.json:/etc/jmcp/devices.json:ro" \
  -v "$PWD/keys:/etc/jmcp/keys:ro" \
  -v "$PWD/jmcp-state:/var/lib/jmcp" \
  rust-junosmcp:local

LXC(Proxmox)

从零构建容器——包括双人模式和实验室模式配对、必须在首次启动前设置正确的凭据模式,以及如何验证 seccomp 配置确实来自随附的单元文件——已记录在 docs/HOW-TO-SETUP-LXC.md 中。

# Build the tarball and checksum.
./scripts/package-lxc.sh

# Verify the checksum.
sha256sum -c dist/rust-junosmcp_0.27.2_amd64.tar.gz.sha256

# Push and install on VM 115. The installer copies the unified binary and unit
# from its extracted package root.
#
# The container MUST be Debian 13 (trixie). This is not a style preference:
# `package-lxc.sh` builds against the glibc of whatever host runs it, and the
# current published binary requires GLIBC_2.39. Debian 12 ships 2.36, so the
# service dies at start with a "GLIBC_2.39 not found" symbol error — after a
# clean build and a clean install, which is the worst place to discover it.
# Debian 13 ships 2.41. Check your own tarball with:
#   objdump -T dist/rust-junosmcp_*/usr/local/bin/rust-junosmcp \
#     | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -Vu | tail -1
#
# Debian 13 also matches docs/PACKAGING.md §2, the container runtime base, and
# rustpanosmcp — one distro generation to track CVEs against, not three.
pct push 115 dist/rust-junosmcp_0.27.2_amd64.tar.gz /tmp/jmcp.tar.gz
pct exec 115 -- bash -c "tar xzf /tmp/jmcp.tar.gz -C /tmp && /tmp/rust-junosmcp_0.27.2_amd64/install.sh"

**改为下载预构建的发布压缩包:**每个 GitHub release 还会发布压缩包、其 .sha256 校验和,以及该压缩包的无密钥 cosign 签名包(.cosign.bundle)。签名由 Sign release tarball 工作流完成,方式与上文中容器镜像的签名相同——不需要密钥对,仅使用 GitHub Actions OIDC。请同时验证校验和和签名;校验和只能证明下载内容在传输过程中未损坏,不能证明它来自本仓库的发布工作流:

version=0.27.2
base="https://github.com/mechubsec/rustjunosmcp/releases/download/v${version}"
curl -LO "${base}/rust-junosmcp_${version}_amd64.tar.gz"
curl -LO "${base}/rust-junosmcp_${version}_amd64.tar.gz.sha256"
curl -LO "${base}/rust-junosmcp_${version}_amd64.tar.gz.cosign.bundle"

sha256sum -c "rust-junosmcp_${version}_amd64.tar.gz.sha256"

cosign verify-blob \
  --certificate-identity "https://github.com/mechubsec/mecmcp/.github/workflows/reusable-sign-release-tarball.yml@f927c820f39369b2601e11e31334cc5b504b1fd1" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --certificate-github-workflow-repository "mechubsec/rustjunosmcp" \
  --certificate-github-workflow-trigger "release" \
  --bundle "rust-junosmcp_${version}_amd64.tar.gz.cosign.bundle" \
  "rust-junosmcp_${version}_amd64.tar.gz"
# Backfilled assets (workflow_dispatch of an already-published tag) use
# --certificate-github-workflow-trigger "workflow_dispatch" instead.

与上文的镜像工作流不同,本仓库自己的 release-sign-tarball.yml 中的 sign 作业会将实际签名工作委托给 mecmcp 的可复用工作流,因此 OIDC 证书的主题是该工作流的路径,并固定为 release-sign-tarball.yml 的 sign: 作业当前通过其 uses: 行引用的确切提交 SHA,而不是本仓库自己的工作流文件,也不是分支引用。每当 sign: 作业重新固定到较新的 mecmcp SHA 或标签时,该固定值都会变化,因此不要指望本 README 中的 SHA 永远准确;请查看 .github/workflows/release-sign-tarball.yml 中的 uses: 行,确认当前固定值。

由于该可复用工作流位于公共仓库中,任何 GitHub 仓库都可以调用它并获得具有相同身份的证书,因此仅凭身份无法证明压缩包来自本仓库的发布。--certificate-github-workflow-repository 和 --certificate-github-workflow-trigger 可弥补这一点:它们会检查证书中的调用仓库和触发事件字段,这些字段必须分别为 mechubsec/rustjunosmcp 和该工作流的两个合法触发条件之一。常规发布使用 release;通过 workflow_dispatch 回填已发布并已签名的标签时使用 workflow_dispatch。如果该 release 已有压缩包签名包,则拒绝执行分发。若 release 有压缩包但没有签名包,仍可回填。该标志要求精确匹配,因此验证回填的资源时请替换该标志。不要省略任何一个标志。

只要身份、颁发者、调用仓库或触发条件不匹配,或者压缩包与签名包不匹配,cosign verify-blob 就会以非零状态码退出——因此此处验证失败意味着不要安装,而不是“可能没问题”。

编辑清单:

# Copy the example and configure your devices.
pct exec 115 -- cp /etc/jmcp/devices.json.example /etc/jmcp/devices.json
pct exec 115 -- $EDITOR /etc/jmcp/devices.json  # or edit via another method
pct exec 115 -- chown jmcp:jmcp /etc/jmcp/devices.json
pct exec 115 -- chmod 0600 /etc/jmcp/devices.json

生成不记名令牌:

# A wildcard tool scope grants read-only tools only; write tools must be named
# explicitly. See "Upgrading to v0.10" and "Tool scopes and write tools" below.
pct exec 115 -- runuser -u jmcp -- /usr/local/bin/rust-junosmcp token add \
  --tokens-file /etc/jmcp/tokens.json \
  --name ops \
  --routers '*' \
  --tools get_router_list,gather_device_facts,execute_junos_command,get_junos_config,commit_check_config,load_and_commit_config

启动服务:

pct exec 115 -- systemctl enable --now rust-junosmcp

安装程序具有幂等性:重新运行会升级二进制文件和单元文件,但不会覆盖 devices.json、tokens.json 或 known_hosts。它会在更改系统状态前验证整个压缩包。打包的服务会在 127.0.0.1:30030/mcp 暴露所有已启用的 Junos 和 SRX 工具,并要求进行不记名令牌身份验证。远程客户端请使用 SSH 隧道或 TLS 反向代理。

清除卡住的操作:state resolve 可处理失败应用后卡在非终止状态的操作记录。这样的记录会阻止之后对该设备进行的所有更改,并且没有任何工具可以清除它——cancel_junos_ change_set 会拒绝终止状态的变更集。请先停止服务,否则运行中的服务会用内存中的数据覆盖该文件:

systemctl stop rust-junosmcp
runuser -u jmcp -- /usr/local/bin/rust-junosmcp state resolve \
  --state-file /var/lib/jmcp/changeset-state.json \
  --operation-id <64-hex-operation-id> \
  --disposition discarded \
  --confirmation "RESOLVED <64-hex-operation-id> AS DISCARDED"
systemctl start rust-junosmcp

确认字符串必须完全匹配:它表示操作人员已检查设备,并确认实际情况如此。只有设备自身的提交日志证明更改已生效时,才使用 committed。 收窄现有令牌的权限范围: token set-scope 可在不重新签发密钥的情况下更改令牌的路由器或工具权限范围,因此客户端可以继续使用同一个 bearer token。适用于移除访问权限而无需重新配置每个客户端。示例请参阅令牌管理部分。

从拆分服务版本升级时,会移除已弃用的 rust-srxmcp 可执行文件、单元和已启用服务链接。升级会有意保留 /var/lib/jmcp/srx-staging/bundles 下现有的支持包。

远程传输 + 身份验证

创建令牌
cargo run -- token add \
  --tokens-file tokens.json \
  --name ops \
  --routers '*' \
  --tools execute_junos_command,gather_device_facts

get_router_list 应用与设备工具相同的路由器权限范围。经过身份验证的允许列表令牌只会收到其权限范围内当前库存中的名称;过期的权限范围条目和被排除的路由器会被省略,不会返回数量或错误。允许列表为空或交集为空时会返回 []。通配符令牌、本地 stdio 和明确未进行身份验证的回环模式仍会保留完整库存。

注意: 文件格式请参阅 tokens-template.json。请使用 token add,不要手动编辑文件——digest 字段必须是带版本标记的 SHA-256 密钥摘要,而不能是明文。规范字段名为 digest 和 devices;仍接受较旧的 hash 和 routers 拼写作为别名,因此现有文件无需更改即可加载。CLI 标志仍为 --routers。

tokens.json 的权限必须为 0600(v0.10.0+)。如果令牌文件可被组或其他用户访问,服务将拒绝启动;错误信息会列出文件所有者的 uid、文件权限、调用进程的 uid,以及要运行的 chmod 命令。每个 token 子命令都会以 0600 权限写入文件,LXC 安装程序也会设置该权限,因此只有手动创建或复制的文件会受到影响。

静态令牌上的设备选择器

token add --device-selector 'site=lab-*'(可重复使用;多个条目按 AND 关系组合)会将令牌的 --devices 限定为 attributes 匹配的库存设备,语法与 OAuth 权限范围映射相同。选择器会在每次身份验证时根据实时库存解析,因此 SIGHUP 或 reload_devices 会在下一个请求时生效;没有标签的设备永远不会匹配,选择器未匹配任何设备的令牌将无法访问任何设备。审批也遵循相同的权限范围:site=lab-* 审批者无法审批 prod 设备上的变更集。

rust-junosmcp token add --tokens-file tokens.json --name lab-ops \
  --devices '*' --tools execute_junos_command --device-selector 'site=lab-*'

先升级所有会读取或重写 tokens.json 的二进制文件——包括共享该存储的每个服务,以及令牌 CLI。不支持 device_selectors 的二进制文件如果只是读取该存储,就会忽略此字段,并立即授予令牌宽泛的 devices 权限范围;较旧的 CLI 还会在执行 token rotate 或 token set-scope 时丢弃此字段。

工具范围与写入工具

工具范围可以是字面值 *,也可以是明确列出的工具名称。两者不能混用——--tools '*',transfer_file 会被拒绝。

通配符工具范围不会授予写入工具权限(v0.10.0+)。"tools": ["*"] 可访问所有只读工具,但不能访问以下 15 个具有写入能力的工具:

写入工具
add_deviceapply_junos_change_set
approve_junos_change_setconfirm_junos_change_set
create_junos_change_setdiscard_candidate
executeload_and_commit_config
manage_appid_signature_packagemanage_idp_security_package
reload_devicesrender_and_apply_j2_template
rollback_configtransfer_file
upgrade_junos

授予写入权限始终需要明确指定工具名称:需要使用 load_and_commit_config 的令牌必须将其列出,同时列出它调用的每个其他工具。明确的允许列表与以往完全相同。

execute 是对 40 个具体操作的严格封装。使用它的令牌必须明确列出 execute 和所选的具体操作;通配符范围不会授予 execute。该封装会原样传递所选操作的参数,因此请使用具体操作的准确参数名称:

{"operation":"gather_device_facts","arguments":{"device":"vsrx-ci"}}

仍支持直接调用全部 40 个具体工具。

tools/list 只公布调用者令牌有权调用的工具,因此 Agent 看到的列表与其实际可调用的工具一致。通配符令牌会看到只读工具,而看不到 15 个具有写入能力的工具;范围为空的令牌会看到空列表。

缓存的列表会过期。 如果客户端在你使用 token set-scope 重新设置令牌范围之前获取了 tools/list,它会一直保留旧视图,直到重新连接——服务器目前不会在 SIGHUP 重新加载时发送 notifications/tools/list_changed。这不会影响授权:无论客户端认为自己能看到什么,调用令牌不再有权使用的工具都会被拒绝。

范围检查仅适用于经过身份验证的 HTTP 调用者。本地 stdio 和 --allow-no-auth 环回例外不会携带调用者上下文,因此不受范围限制。

请以服务用户身份运行令牌子命令。 如果 systemd 单元以专用用户身份运行服务器(例如打包单元中的 User=jmcp),文件 token add/revoke/rotate/set-scope 写入后会继承调用用户的所有权。如果以 root 身份运行,生成的 tokens.json 将为 root:root 0600,服务用户将无法读取它——随后服务器会在启动时不断崩溃并重启,报错 Permission denied。你可以:

# 首选:以服务用户身份运行子命令。
sudo -u jmcp rust-junosmcp token add --tokens-file /etc/jmcp/tokens.json ...

# 或者以 root 身份运行后修复所有权。
rust-junosmcp token add --tokens-file /etc/jmcp/tokens.json ...
chown jmcp:jmcp /etc/jmcp/tokens.json

如果服务器在启动时遇到此情况,现在的错误消息会报告文件的 uid/mode 以及调用者的 uid,因此无需翻查 journald 就能明确如何修复。

使用身份验证运行(streamable-http)
cargo run -- \
  --device-mapping devices.json \
  --transport streamable-http \
  -H 127.0.0.1 \
  -p 8765 \
  --tokens-file tokens.json
环回例外(无身份验证,仅限本地)
cargo run -- --device-mapping devices.json --transport streamable-http \
  -H 127.0.0.1 -p 8765 --allow-no-auth

如果绑定地址不是环回地址,--allow-no-auth 将被拒绝。

非环回连接需要 TLS
cargo run -- \
  --device-mapping devices.json \
  --transport streamable-http \
  -H 0.0.0.0 \
  -p 8765 \
  --tokens-file tokens.json \
  --tls-cert cert.pem \
  --tls-key key.pem

要通过纯 HTTP 绑定到非环回地址(例如,在同一主机上使用 TLS 终止代理),请添加 --allow-insecure-bind。此标志会覆盖 TLS 要求,应谨慎使用——仅在有外部传输安全保障时使用。

主机允许列表(DNS 重绑定防护)

streamable-http 传输会将传入的 Host 标头与允许列表进行校验(默认:仅限环回地址——localhost、127.0.0.1、::1)。这可修复 RUSTSEC-2026-0189(DNS 重绑定)问题。非环回客户端必须通过 --allowed-host <HOST> 加入允许列表(可重复指定),否则无论身份验证状态如何,都会收到 HTTP 403:

cargo run -- \
  --device-mapping devices.json \
  --transport streamable-http \
  -H 0.0.0.0 \
  -p 8765 \
  --tokens-file tokens.json \
  --tls-cert cert.pem \
  --tls-key key.pem \
  --allowed-host jmcp.example.net

无法关闭允许列表。--disable-host-check 已在 0.15.3 中移除,现在会在启动时被拒绝。允许列表是 DNS 重绑定防护(RUSTSEC-2026-0189),而重绑定专门针对绑定到环回地址的服务——浏览器将攻击者控制的名称解析为 127.0.0.1,并通过伪造的 Host 访问服务。因此,“关闭”恰恰在看起来最安全的地方最危险。如果客户端发送的 authority 不为服务所知,请用 --allowed-host 指定;该标志可重复使用。

OAuth 2.1 资源服务器模式(选择性构建)

外部授权服务器的 JWT 访问令牌可以与静态令牌一同接受(mecmcp#498)。已发布的二进制文件未编译此功能:使用 cargo build --release -p rust-junosmcp --features oauth-resource 构建;未启用此功能构建的二进制文件既没有 --oauth-* 标志,也没有发现路由。会先尝试静态令牌,并且必须提供 --tokens-file。

rust-junosmcp --transport streamable-http --tokens-file /etc/jmcp/tokens.json \
  --oauth-issuer https://idp.lab.example/realms/lab \
  --oauth-resource https://junos.lab.example/mcp \
  --oauth-scope-map /etc/jmcp/oauth-scopes.json

添加 --oauth-max-token-lifetime-secs <N>,即可拒绝 exp - iat 超过 N 秒的任何访问令牌,从而限制用户离职后其令牌仍可使用的时长(默认关闭)。

如需按资产属性而非名称授予权限,可在作用域映射条目中添加 device_selectors(site=、vendor=、tag=、* glob;各项按 AND 关系组合)。这些选择器会进一步缩小该条目的 devices 范围,并在每次身份验证时根据实时资产清单进行解析,因此 SIGHUP 或 reload_devices 会在下一个请求时生效:

{"version": 1, "scopes": {
  "junos:lab": {"tools": ["get_junos_config"], "devices": ["*"],
                "device_selectors": ["site=lab-*"]}}}

属性来自 devices.json 中的 attributes 对象;没有该对象的设备不会匹配任何选择器(默认拒绝)。有关完整的选择器语法,请参阅 mecmcp 的 OAUTH.md。

升级到 mecmcp 0.29 时,oauth: 前缀将被保留:请先重命名任何使用该名称的静态令牌,否则整个令牌文件都无法加载。作用域映射格式、IdP 设置、受众规则和回滚说明见 mecmcp 的 OAUTH.md。

热重载

撤销或轮换令牌后,服务会在不重启的情况下重新加载令牌存储。对任何写入子命令传入 --server-pid <pid>,文件写入后便会自动发送 SIGHUP:

# Revoke — writes file, then signals the server.
cargo run -- token revoke --tokens-file tokens.json --name ops --server-pid <pid>

# Rotate (mints a new secret, preserves scopes) — same pattern.
cargo run -- token rotate --tokens-file tokens.json --name ops --server-pid <pid>

# Add a new token and signal in one step.
cargo run -- token add \
  --tokens-file tokens.json \
  --name ops2 \
  --routers '*' \
  --tools execute_junos_command,gather_device_facts \
  --server-pid <pid>
无需重新签发密钥即可更改令牌作用域

token set-scope 会就地更改现有令牌的作用域。摘要、created_at 和封装版本都会保留,因此持有密钥的客户端可以继续使用——这与 rotate 不同,后者会生成新密钥,并要求立即更新所有使用方。

# Narrow tools; leave the router scope alone.
cargo run -- token set-scope \
  --tokens-file tokens.json \
  --name ops \
  --tools gather_device_facts,get_junos_config,load_and_commit_config \
  --server-pid <pid>

# Narrow routers; leave the tool scope alone.
cargo run -- token set-scope \
  --tokens-file tokens.json \
  --name ops \
  --routers edge-1,edge-2 \
  --server-pid <pid>

--routers 和 --tools 均为可选且彼此独立;省略的选项保持不变。两者都不提供会导致错误。命令会将最终作用域输出到 stderr;文件写入后,--server-pid 会发送常规 SIGHUP。未知工具名称会被拒绝;未知设备名称会发出警告,但仍会接受,因为令牌操作发生在加载资产清单之前。

如果需要在不更改令牌的情况下触发重载(例如,手动编辑文件后),请直接发送 SIGHUP:

kill -HUP <pid>
拒绝矩阵
标志绑定地址结果
(无)任意拒绝 — streamable-http 需要 --tokens-file 或 --allow-no-auth
仅 --allow-no-auth非环回地址拒绝 — --allow-no-auth 仅限环回地址
仅 --allow-no-auth环回地址正常 — 但请注意:即使同时提供 --tls-cert/--tls-key,身份验证仍处于禁用状态;TLS 可提供机密性,但任何能够访问该端口的客户端都拥有完整的工具访问权限(存在误用风险)
仅 --tokens-file非环回地址,无 TLS拒绝 — 添加 --tls-cert/--tls-key 或 --allow-insecure-bind
--tokens-file --allow-insecure-bind非环回地址,无 TLS正常 — 会检查令牌;你需要确保外部传输安全
--tokens-file --tls-cert cert.pem --tls-key key.pem任意正常
规范环境变量

统一服务使用一个 JMCP_* 命名空间。命令行值优先于环境变量值。

用途规范环境变量
监听器JMCP_HTTP_HOST、JMCP_HTTP_PORT
清单和令牌JMCP_DEVICES_PATH、JMCP_TOKENS_PATH
TLSJMCP_TLS_CERT、JMCP_TLS_KEY
破坏性操作租约JMCP_DEVICE_LEASE_DIR
支持包暂存JMCP_SUPPORT_BUNDLE_STAGING_DIR、JMCP_SUPPORT_BUNDLE_STAGING_MAX_BYTES
审计JMCP_AUDIT_FORMAT、JMCP_AUDIT_LOG_FILE、JMCP_AUDIT_JOURNALD、JMCP_AUDIT_REDACT、JMCP_AUDIT_HMAC_KEY_FILE
指标JMCP_ENABLE_METRICS

为便于迁移,仅当相应的命令行选项和规范变量都未设置时,才会接受旧版 JMCP_SRX_* 别名,并在 0.8.0 中发出警告。JMCP_SRX_HTTP_PORT 始终会被忽略:已停用的第二个监听器不复存在。请立即将部署迁移到规范名称。

升级到 v0.10

v0.10.0 引入了两项不兼容的授权变更。新二进制文件会在运行时检查这两项,因此请在 0.9.x 仍在运行时完成准备工作,否则升级会导致启动失败,或在无提示的情况下破坏具有写入能力的客户端。

1. 修复令牌文件权限

如果 tokens.json 可被组或其他用户访问,新二进制文件将拒绝启动。在替换二进制文件前,请检查并修复权限:

# On the server host, as the service user.
stat -c '%a %U:%G %n' /etc/jmcp/tokens.json
chmod 0600 /etc/jmcp/tokens.json

LXC 安装程序已设置 0600,因此打包安装几乎肯定没有问题。需要检查的是手动创建、在主机间复制、从备份恢复或由配置管理工具写入的文件。如果服务遇到此问题,启动错误会指出文件所有者的 uid、文件模式、调用进程的 uid,以及确切的 chmod 命令;不会静默失败。

2. 为需要写入工具的通配符令牌重新指定范围

通配符工具范围不再授予对 15 个具有写入能力的工具的访问权限。升级后,任何使用 "tools": ["*"] 并调用其中某个工具的令牌,都会开始收到 ToolNotInScope 拒绝。

查找受影响的令牌:

sudo -u jmcp rust-junosmcp token list --tokens-file /etc/jmcp/tokens.json

TOOLS 列显示 * 的每一行都是候选项。请逐一判断它是否确实需要写入权限:

  • 实际只读 — 保持不变。通配符范围仍可访问所有只读工具,而且现在比以前更安全。
  • 需要写入工具 — 将通配符替换为明确列出其调用的所有工具的列表,包括只读工具。范围不能同时包含 * 和具体名称。

token set-scope 可以在不重新签发密钥的情况下完成此操作,因此客户端在变更期间仍可正常工作:

sudo -u jmcp rust-junosmcp token set-scope \
  --tokens-file /etc/jmcp/tokens.json \
  --name ops \
  --tools get_router_list,gather_device_facts,get_junos_config,junos_config_diff,commit_check_config,load_and_commit_config \
  --server-pid "$(systemctl show -p MainPID --value rust-junosmcp)"

set-scope 适用于 0.10.0 及更高版本。在 0.9.x 上,可以手动编辑 tokens.json 来实现同样的效果——只修改 tools 数组,保留 hash/digest 和 created_at 不变——然后运行 systemctl kill -s HUP rust-junosmcp。不要为此使用 rotate:它会生成新密钥,并立即使所有使用该令牌的使用方无法访问。

3. 升级,然后验证

安装新版本后,确认存储已加载,且作用域符合预期:

systemctl status rust-junosmcp
sudo -u jmcp rust-junosmcp token list --tokens-file /etc/jmcp/tokens.json

然后通过受影响的客户端调用一个写入工具进行测试。如果此时遭到拒绝,说明该令牌的允许列表中缺少该工具名称。

升级前请备份 tokens.json ——回滚并非对称操作。 v0.10 会读取旧的 hash/routers 拼写,但任何 v0.10 令牌写入操作(add、rotate、revoke、set-scope)都会将整个文件改写为规范的 digest/devices 拼写,而 0.9.x 无法解析该格式——它要求使用 hash,并会因无法加载存储而失败,因此回滚后的服务将无法启动。请保留一份副本:

sudo -u jmcp cp -a /etc/jmcp/tokens.json /etc/jmcp/tokens.json.0.9-backup

如果你尚未运行 v0.10 令牌写入操作,该文件的字节内容完全相同,回滚即可顺利完成。否则,请还原备份(升级本身不会更改密钥和作用域),或手动将这两个字段改回原名称。在 Proxmox 上,安装前对容器创建 LXC 快照也能一并保护其他所有内容。

资源限制(streamable-HTTP)

统一端点会强制执行可配置的 DoS 防护限制。大多数限制默认启用,且阈值较宽松;可选的每令牌请求速率限制器在其两个参数均为正数之前处于禁用状态。单项限制设为 0 时即表示禁用,但须遵守下方的速率/突发配对规则。

标志环境变量默认值作用
--max-request-body-bytesJMCP_MAX_REQUEST_BODY_BYTES10 MiB在缓冲前拒绝更大的请求体,返回 413
--max-inflight-requestsJMCP_MAX_INFLIGHT_REQUESTS64全局并发上限;超限 → 503
--max-inflight-requests-per-tokenJMCP_MAX_INFLIGHT_REQUESTS_PER_TOKEN16每令牌并发上限 → 503
--max-requests-per-second-per-tokenJMCP_MAX_REQUESTS_PER_SECOND_PER_TOKEN0每令牌补充速率;须与突发量配对(0/0 表示禁用)
--max-request-burst-per-tokenJMCP_MAX_REQUEST_BURST_PER_TOKEN0每令牌即时突发量;须与速率配对(0/0 表示禁用)
--max-inflight-requests-per-routerJMCP_MAX_INFLIGHT_REQUESTS_PER_ROUTER4每个路由器的并发上限 → 503
--max-sessionsJMCP_MAX_SESSIONS128会话数量上限 → 503
--max-sessions-per-tokenJMCP_MAX_SESSIONS_PER_TOKEN16每个持有者令牌的会话上限 → 503
--session-idle-timeout-secsJMCP_SESSION_IDLE_TIMEOUT_SECS300清理空闲会话
--session-max-lifetime-secsJMCP_SESSION_MAX_LIFETIME_SECS3600清理超期会话

全局会话上限会在创建会话时以原子方式强制执行。中间件会尽早拒绝明显超出容量的请求;同时,共享会话管理器会关闭并发创建中未能抢到最后一个名额的会话,并返回相同的 session_cap 503 响应约定。初始化被拒绝时不会返回 Mcp-Session-Id;关闭或清理已准入的会话后,其名额会被释放。

每令牌会话计数使用经过身份验证的确切令牌名称。初始化成功后会绑定返回的 Mcp-Session-Id;显式关闭以及因空闲或生命周期超时而清理会话时,都会释放名额。达到上限时返回 {"error":"overloaded","limit":"token_session_cap"}。显式无身份验证模式下,由于不存在令牌身份,因此不会应用此上限。

超限响应会携带 Retry-After: 1。响应流结束时会释放并发许可。多路由器调用会为每个唯一的顶层 router、router_name、routers 或 router_names 目标各占用一个路由器名额。 每路由器达到上限时,响应正文会标明 {"error":"overloaded","limit":"router_concurrency"}。 每个路由器的 HTTP 许可会在破坏性工作流等待跨进程设备租约之前获取。破坏性调用在等待或持有该租约期间计为一次;HTTP 上限同时约束读取请求和等待中的破坏性请求,而租约仍是跨进程串行执行破坏性操作的依据。

按令牌进行的请求速率限制是可选的令牌桶,按经过身份验证的令牌名称精确匹配。将每秒请求数速率和突发量都设为正值即可启用;将两者都设为 0 即可禁用。只提供一个正值会导致启动失败。每个经过身份验证的 /mcp HTTP 请求消耗一个令牌。令牌桶耗尽时,会在获取并发或会话容量之前返回 429、Retry-After: 1 和 {"error":"rate_limited","limit":"token_rate"}。显式的无身份验证模式会跳过此按令牌控制。

使用 RPS 限流器吸收大量廉价、短时调用带来的突发流量。使用并发限制约束同时进行的高开销 NETCONF/SSH 工作和缓慢的响应流。两者相互独立,可同时启用;先执行速率检查,而并发或会话容量耗尽时仍沿用现有的 503 约定。

Prometheus 导出为可选功能,通过 --enable-metrics(JMCP_ENABLE_METRICS)启用。它会在 /mcp 旁挂载一个无需身份验证的 GET /metrics;请使用网络控制保护此端点。有关抓取配置、指标名称、标签和 PromQL 示例,请参阅 Prometheus 指标。

CLI

Junos MCP server (Rust)

Usage: rust-junosmcp [OPTIONS] [COMMAND]

Commands:
  token  Manage the bearer-token store
  help   Print this message or the help of the given subcommand(s)

Options:
  -f, --device-mapping <DEVICE_MAPPING>
          JSON file with device mapping (Juniper junos-mcp-server compatible) [default: devices.json]
  -t, --transport <TRANSPORT>
          Transport [default: stdio] [possible values: stdio, streamable-http]
  -H, --host <HOST>
          Bind host (streamable-http only) [default: 127.0.0.1]
  -p, --port <PORT>
          Bind port (streamable-http only) [default: 30030]
      --tokens-file <TOKENS_FILE>
          Bearer-token file. Required for streamable-http unless --allow-no-auth
      --tls-cert <TLS_CERT>
          PEM-encoded TLS cert (streamable-http only). Pair with --tls-key
      --tls-key <TLS_KEY>
          PEM-encoded TLS key (streamable-http only). Pair with --tls-cert
      --allow-no-auth
          Disable bearer-token auth. Refuses to bind off-loopback
      --allow-insecure-bind
          Bind off-loopback over plain HTTP. Required for non-127.0.0.1 hosts when TLS is not configured
      --inventory-readonly
          Reject add_device and reload_devices unconditionally
      --allow-password-auth-add
          Permit add_device to accept auth.type=password (mutually exclusive
          with --inventory-readonly)
      --device-lease-dir <DEVICE_LEASE_DIR>
          Shared directory for cross-process destructive-operation leases
          [default: /var/lib/jmcp/device-leases]
      --allowed-host <HOST>
          Additional Host authorities to accept on the streamable-http
          endpoint, beyond the loopback defaults (localhost, 127.0.0.1, ::1).
          Repeatable
  -h, --help
          Print help
  -V, --version
          Print version

针对真实设备进行测试

JMCP_TEST_HOST=10.0.0.1 \
JMCP_TEST_USER=admin \
JMCP_TEST_PASS=secret \
cargo test -p rust-junosmcp-core --test integration_real_device -- --ignored --nocapture

将审计记录转发到事件存储

审计轨迹不会留在此主机上。本服务器遵循该系列标准 — AUDIT-FORWARDING-STANDARD.md。

只存在于生成记录的机器上的审计记录并不构成审计轨迹:它只是某台机器上的日志文件,而该机器的操作员正是记录所涉及的一方。

发出记录(现已生效)
--audit-format json \
--audit-log-file /var/lib/jmcp/audit.jsonl

JSON 是必需格式。text 格式供在终端中阅读,不用于解析。文件是面向操作员的产物,必须进行轮转 — 服务器绝不会截断它。

传输(已规定,尚未实现)

根据 SSDF 合并后的证据约定,记录会直接写入 SSDF 的 ssdf.audit,作为哈希链式行,以便检测行的删除或编辑。进度跟踪见 mecmcp#292。

我们曾设计成本更低的 syslog 路径,但最终否决:它虽然可行,但记录没有链式关联;而这里其他环节都通过设计具备防篡改能力 — 计划摘要绑定审批,审批标明不同的主体,token_verified_fields 将经背书的来源信息与声明的信息区分开来。最后一跳若不采用链式关联,恰恰会在审计员最需要保障的地方丢弃这一保证。相关理由已记录在标准中。

查看结果

token_verified_fields 列出由令牌背书的来源字段。同一组中的其余字段 — client_name、model_id、session_id — 均由客户端自行声明,且未经任何验证。不要将它们视为等同字段。

request_id 用于关联传输事件、处理程序事件,以及(在 Junos 上)设备提交注释。

许可证

根据 MIT 许可。


README 配图
a mechub project · 确定性决策 · 模型解释 · 人类审批
github.com/fastrevmd-lab

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。