阅读需 8 分钟

本地代理操作网关健康检查

本地代理操作网关健康检查,用于验证应用状态、保险库拒绝、HTTP 和 SSH 金丝雀,以及审计完整性,同时避免泄露凭据。

本地代理操作网关健康检查

本地操作网关只有在能够拒绝错误操作、执行正确的受控操作,并留下经得起审查的证据时,才算健康。绿色的进程监控几乎无法证明这些事情。它只能告诉你某个东西拥有一个 PID,却不能告诉你保险库是否锁定、代理是否能够连接网关、注入的凭据是否仍然有效,或者审计链是否记录了这次调用。

Sallyport 中的情况尤其需要区分,因为应用、保险库闸门、授权决定、外部操作和加密审计日志都是独立的故障点。把它们当成一个笼统的“健康”信号,会造成最糟糕的监控:控制措施已经失效,监控却仍然显示绿色。

把它做成一个小型验收套件,而不是一堆端口检查。套件应使用无害的金丝雀目标,返回非秘密证据,并正确识别保险库锁定的状态。如果必须由人员通过 Touch ID 解锁保险库,监控就应明确报告这一状态,而不是试图绕过它。

应用正在运行只是第一个条件

可用性探针应该只回答一个狭窄的问题:本地 Mac 能否启动并让网关应用持续运行?它不应假装运行中的进程就能证明外部操作有效。

对于菜单栏应用,先从监督程序可以在不接触凭据的情况下运行的本地检查开始。确切的进程名称和安装位置可能因版本而变化,因此应将这些值放在一个本地配置文件中,而不是散落在各个脚本里。基础 shell 探针可以这样写:

#!/bin/sh
set -eu

APP_NAME="Sallyport"

if ! pgrep -x "$APP_NAME" >/dev/null 2>&1; then
  open -a "$APP_NAME"
  sleep 2
fi

if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
  printf 'app=ready\n'
  exit 0
fi

printf 'app=unavailable\n' >&2
exit 2

这个检查有意只做出有限的声明。open -a 请求 macOS 启动应用,pgrep 则在请求后观察进程。它不能证明应用已经完成初始化,不能证明保险库能够响应请求,也不能证明 MCP shim 可以接受客户端连接。Apple 将 Launch Services 说明为用于启动和激活应用的系统接口,因此通过操作系统启动应用,比把应用包路径硬编码进脚本更好。

让输出保持结构化且简单。对调度器来说,app=ready 已经足够。不要把 ps 输出写入集中式日志,否则命令参数、用户名或无关的进程细节会永久堆积。

进程可能存在,但已经卡死。它也可能因为 Mac 处于睡眠、用户已退出登录,或更新后正在重启而暂时不存在。告警规则需要为这些预期情况设置宽限期。每次笔记本合上盖子都触发告警的健康检查很快就会被禁用,到真正发生故障时也就帮不上忙了。

因此,有用的可用性信号应该是本地且有边界的:应用已启动,在短暂的稳定等待期后仍然存在,并且 MCP 客户端可以开始会话。最后一项应放在单独的探针中,因为它测试的是另一条边界。

保险库状态必须是明确的结果

保险库锁定时拒绝操作,这本身是健康的。把这个状态称为服务中断,会混淆安全行为与服务故障。

Sallyport 的保险库闸门规则是绝对的:锁定期间,所有操作都会被拒绝。在受支持的 macOS 硬件上,保险库闸门使用 Secure Enclave 和 Touch ID。检查必须保留这条规则。不要编写向界面输入密码的脚本,不要保存绕过生物识别的令牌,也不要把已解锁的桌面会话当成应该解锁保险库的证明。

不要只使用布尔值,而应使用四种结果状态:

  • ready:应用可用,保险库已解锁,可以运行受控检查。
  • locked:应用可用,但保险库正确地拒绝了操作。
  • denied-unexpectedly:保险库已解锁,但预期的金丝雀操作却被拒绝。
  • unavailable:应用或其本地 MCP 路径无法响应。

这套词汇可以避免一种常见的运维错误。团队经常在夜间安排需要凭据的探针,屏幕锁定后看到检查失败,于是不断削弱系统,直到系统能够自行解锁。他们并没有修好监控,而是移除了保险库原本要求人工做出的决定。

实际做法应分两部分运行。无人值守的任务记录应用可用性和锁定保险库的拒绝结果。人员,或已经得到人工批准的受控工作站会话,在解锁后启动需要凭据的检查。将原因写入运行输出:

{
  "run_id": "hc-2026-07-22T141501Z-8f29",
  "app": "ready",
  "vault": "locked",
  "http": "skipped",
  "ssh": "skipped",
  "audit": "verified",
  "reason": "credentialed checks require an unlocked vault"
}

运行 ID 不是秘密。它让操作人员之后可以用一个稳定的值与活动记录进行比对。不要使用用户名、机器序列号、端点 URL 或凭据标签作为运行 ID。

每次会话授权和每次调用审批也需要单独处理。新的代理进程可能需要会话审批,某个凭据也可能要求每次使用都获得审批。这是预期行为,不是不稳定的测试。运行器应说明它是为已批准的会话设计的,还是每次调用都由人员审批。无声超时不会给下一位调查人员留下任何有用信息。

使用能够证明凭据注入的金丝雀目标

HTTP 检查应调用专门用于验证某个金丝雀凭据的端点,并返回固定的非秘密结果。测试公共 URL 只能证明网络可用,无法说明网关是否选中了预期凭据、是否将凭据插入正确的标头,或是否将凭据与代理隔离。

建立一个小型服务,只接受一个路径、一种方法和一种凭据形式。在服务端保存预期的金丝雀秘密,在网关保险库中保存同一个金丝雀秘密。触发操作的客户端不应收到该秘密,服务也不应回显它。

响应契约可以简单到这样:

{
  "check": "agent-gateway-http",
  "result": "ok",
  "request_id": "7d7a0f3c"
}

凭据缺失或错误时,服务应返回 401;方法错误时返回 405;只有在收到预期凭据时才返回 200。在服务端生成 request_id,并保持其不透明。不要根据授权标头或传入请求的任何部分生成它。

使用专用路由,例如 /agent-gateway-canary。不要把检查附加到现有生产端点上。生产端点会随着时间积累速率限制、重定向、内容协商、缓存规则、计费副作用和权限变更。金丝雀路由则可以始终保持有意的简单。

RFC 9110 定义了请求方法的语义,并将 GET、HEAD、OPTIONS 和 TRACE 归为安全方法。但 HTTP 中的“安全”意味着请求的操作不应改变资源的预期状态,并不意味着它不会影响账户、日志、配额或下游行为。API 可以记录 GET 请求、收取请求费用,或者因为实现不佳而触发副作用。应建立一个能够检查服务器端行为的路由,而不是仅仅相信熟悉的方法名。

一个常见但糟糕的建议是使用带生产 API 令牌的 curl 作为健康检查。它之所以流行,是因为只需一行命令。但这会出错,因为 shell 历史、进程检查、CI 日志和错误输出都会给 bearer token 提供太多可能出现的位置。如果网关通常会自己注入凭据,这种做法还绕过了你真正需要测试的行为。

运行器应通过代理使用的同一 MCP 路径调用网关。不要为检查另造一个后门 HTTP 客户端。将具体传输方式的调用封装在本地适配器后面,因为工具名称和请求格式可能变化。适配器接收一个逻辑操作,请求 MCP 连接的网关执行它,然后只输出标准化结果。

{
  "action": "http_canary",
  "target": "canary-api",
  "method": "POST",
  "path": "/agent-gateway-canary",
  "expected_status": 200,
  "expected_check": "agent-gateway-http"
}

适配器必须在失败时进行脱敏。它可以报告 http_status=401transport_error=timeoutresponse_schema=invalid,但不能打印传出标头、请求正文、带查询参数的完整 URL 或原始响应,除非你已经确认这些数据不会泄露敏感信息。

HTTP 成功必须证明预期的操作

单独一个 200 证据很弱。检查必须验证服务响应、请求方法和目标身份,避免重定向、代理页面或过期测试夹具造成误通过。

让金丝雀响应能够识别这次测试,但不要识别凭据。比较少量精确字段:

status=200
check=agent-gateway-http
result=ok
request_id=7d7a0f3c

检查器应接受语法有效的 request_id,然后将其与运行 ID 一起保存。对于字段缺失、正文声称成功但状态码不是 2xx,以及内容类型不符合预期的情况,都应判定失败。强制门户、企业代理错误页面或错误的 DNS 记录都可能返回有效的 HTTP 响应。这只是传输成功,不是操作成功。

curl 文档也指出了相关问题:如果没有使用 --fail--fail-with-body,curl 不会将 404 或 401 这样的 HTTP 状态视为命令失败。对于通用传输客户端来说,这种行为是合理的,但它会让只看 curl 退出状态来编写监控的人掉入陷阱。如果适配器内部使用 curl,应同时捕获进程结果和 HTTP 状态,再根据明确的契约判断成功。

除非重定向属于端点设计的一部分,否则不要让检查自动跟随重定向。重定向可能将金丝雀调用送到返回 200 的登录页面,也可能送到你本来不想联系的其他主机。在配置中固定 HTTPS 来源,通过正常的客户端栈验证证书。只有在不会暴露敏感网络细节的前提下,才记录最终对端身份。

超时需要单独标记。DNS 失败、TCP 拒绝、TLS 验证失败、网关拒绝、上游 401、上游 500 和响应不匹配,分别对应不同的责任方。如果运行器把它们全部称为 http=failed,每次事故开始后的前十分钟都会浪费在确认请求究竟停在哪里。

有用的失败记录可以这样写:

{
  "run_id": "hc-2026-07-22T141501Z-8f29",
  "check": "http_canary",
  "outcome": "failed",
  "stage": "upstream_response",
  "http_status": 401,
  "request_id": null,
  "secret_material": "redacted"
}

secret_material 这一行只是提醒阅读记录的人,并不能证明脱敏真的成功。应通过测试证明脱敏有效:故意让金丝雀拒绝凭据,捕获运行器的标准输出和标准错误,然后在这些文件中搜索测试秘密。秘密不应出现。还要对格式错误的 JSON、超时、TLS 失败和代理侧工具错误重复测试。秘密最容易从错误路径中泄露。

SSH 需要隔离目标,而不是登录 shell

测试 SSH 边界
Sallyport 内置的 sp-ssh 辅助程序可以执行 SSH 操作,而不会把 SSH 密钥交给代理。

SSH 健康检查应证明对专用账户的身份验证和命令执行,而服务器应拒绝运行任意命令。连接到普通管理主机虽然能够证明连接成功,却朝错误的方向提供了过多权限,让健康检查凭据拥有可用的 shell。

在受控测试主机上创建单独的账户,例如 gateway-health。在 authorized_keys 中配置强制命令,或使用等效的服务器端限制。强制命令应忽略原始命令,将时间戳和不透明的运行 ID 写入本地审计文件,然后返回固定响应。

概念性的 authorized_keys 条目如下:

command="/usr/local/libexec/gateway-health",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA... gateway-health

公钥属于网关的金丝雀身份,私钥保存在保险库中。这里的 AAAA... 文本有意不完整,因为你必须生成自己的密钥材料,不能把示例直接复制到生产文件中。

服务器命令应避免回显 $SSH_ORIGINAL_COMMAND、环境变量或认证细节。它可以返回固定格式:

{"check":"agent-gateway-ssh","result":"ok","receipt":"c2b91a"}

如果需要运行 ID 进行关联,可以在提交的命令中传入不透明令牌,并让强制命令验证固定长度的十六进制字符等严格格式。绝不要接受任意字符串,再把它写入 shell 命令、文件名或日志行。更好的做法是让服务器生成收据,在调查时按时间窗口关联记录。

如果服务器允许交互式 shell、端口转发、分配 TTY,或执行强制命令之外的命令,检查都应失败。这些属于配置回归。它们改变了金丝雀凭据的暴露范围,因此严重程度应不同于普通网络超时。

不要复用部署所用的 SSH 身份。复用身份起初更容易设置,最终却会把一次监控故障变成广泛的访问问题。健康凭据只应有一个用途、一个目标账户,并且只能用于生成收据。

Sallyport 通过内置的无状态 sp-ssh 辅助程序发送 SSH。你的测试因此应经过标准网关操作路径,而不是用 OpenSSH 调用本地私钥。否则你检查到的只是主机和账户,跳过了真正需要证明的凭据边界。

审计验证是另一项独立断言

金丝雀操作成功和审计链有效,是两项不同的声明。两者都要验证。

活动记录告诉你网关记录了一次具体操作。会话记录告诉你代理运行的情况,并提供撤销该运行的方法。两者都不能替代对远程结果的检查,因为请求可能在上游失败之前就已经被记录。反过来,远程系统可能收到请求,而本地日志路径恰好在此时失败。你需要的是关联,而不是想当然地认为两者总会一致。

对于每次成功的 HTTP 或 SSH 操作,收集三项非秘密证据:

  • 运行器生成的健康运行 ID;
  • 金丝雀服务生成的不透明收据或请求 ID;
  • 以 UTC 记录的本地操作时间戳。

然后检查活动日志中可以安全保留的操作元数据,例如通道、操作结果、目标别名和时间。不要期待日志中出现明文凭据,它不应该出现。也不要要求日志重现远程响应正文。收据属于远程金丝雀系统,不属于网关的秘密存储。

Sallyport 从一个只写不可读、采用哈希链的加密审计日志中生成 Sessions 和 Activity 日志。将其离线完整性检查作为套件中的独立部分运行:

sp audit verify

成功的命令应记录为 audit=verified;非零结果在证明并非如此之前都应视为完整性事件。这项验证不需要保险库密钥,因此即使保险库锁定,也适合无人值守的本地检查。

不要把审计验证简化为“日志文件存在”。文件存在几乎不能说明什么。文件截断、记录被替换、链断裂,或写入程序启动后就停止,都可能与 Finder 中看起来正常的路径同时存在。

还有一个容易犯的错误:只检查审计链是否有效。即使链有效,也可能缺少你期待的事件,因为网关从未尝试执行该操作。每次金丝雀操作后,都应将链验证与事件存在性断言结合起来。为本地投影设置合理的到达窗口,并将延迟与事件缺失分开报告。日志更新延迟可能需要调查,但它与写入失败不是一回事。

让运行器位于所检查的信任边界之外

使用标准 MCP 路径
内置的 sp mcp shim 让健康检查客户端使用与代理相同的 MCP 边界。

健康检查运行器应该编排操作并判断证据。它不应持有凭据、解析保险库文件或读取包含秘密的应用状态。

一种实用布局包含四个组件:

  1. 本地调度器在专用 macOS 账户下启动小型运行器。
  2. 运行器检查应用是否存在,并调用本地 MCP 适配器。
  3. 适配器通过网关请求命名的金丝雀操作,并返回经过脱敏的结构化结果。
  4. 运行器验证审计完整性,并将一份简短报告写入受保护的本地目录。

专用账户不应有权限访问保险库数据文件。Apple 将非沙盒 macOS 应用支持数据放在当前用户的 ~/Library/Application Support 目录下,但监控程序不应假设自己可以或应该检查这些文件。检查需要验证行为,而不是复制保险库内容。

让配置保持声明式且不含秘密。下面的示例为运行器提供了验证结果所需的信息,但不会暴露凭据:

checks:
  http_canary:
    target_alias: canary-api
    expected_status: 200
    expected_check: agent-gateway-http
    timeout_seconds: 10
  ssh_canary:
    target_alias: canary-ssh
    expected_check: agent-gateway-ssh
    timeout_seconds: 10
  audit:
    command: sp audit verify
    timeout_seconds: 15

目标别名很重要。别名比完整 URL 或主机名更不敏感,还会迫使你在网关配置中明确选择目标。如果操作人员修改了别名,操作检查应表明在悄悄开始测试另一个位置之前,需要进行配置审查。

不要给运行器审批新代理进程的权限。会话授权默认开启是有原因的。新代理进程的首次调用会在审批卡中显示其代码签名机构,审批只对本次运行有效。如果需要在人员批准后安排定期检查,就安排一个经过审查的长期健康客户端进程。进程退出后,应预期下一次运行会再次请求审批。

这比隐藏一个始终获批的自动化账户稍微不方便,却能避免新可执行文件仅仅因为复制了健康检查的命令行,就获得操作权限。

失败检查需要给出有用的诊断

撤销可疑的代理运行
当代理行为需要调查时,可以使用 Sessions 日志立即撤销这次代理运行。

大多数网关健康检查失败都源于普通的配置漂移。危险之处在于,人们常常通过削弱控制措施来回应,而不是定位发生故障的边界。

考虑一个看起来真实的过程:应用正在运行,保险库已解锁,HTTP 金丝雀返回 401。第一反应往往是把金丝雀令牌粘贴到 shell 中,直接调用端点。不要这样做。这只能证明令牌有效,却绕过了保险库,并创造了新的秘密泄露路径。

应按以下顺序处理证据:

  1. 确认本地 MCP 适配器确实到达网关,并收到操作结果,而不是在提交之前就超时。
  2. 确认目标别名仍然指向预期的已保存凭据和端点配置。
  3. 检查金丝雀服务日志中的不透明请求 ID、方法、路由和拒绝原因。不要记录收到的授权值。
  4. 检查活动日志中是否有匹配的操作尝试,并使用 sp audit verify 验证审计链。
  5. 如果配置审查显示预期值发生变化,或秘密可能已经暴露,就轮换金丝雀凭据。

这个顺序可以区分四种看起来完全相同的故障:目标映射错误、秘密被撤销或轮换、网关注入失败,以及上游服务配置发生变化。它还可以避免一种常见的自我造成的事故:操作人员在终端中“测试”秘密,随后发现秘密被复制到 shell 历史、滚动缓冲区或支持包中。

根据失效的控制措施划分严重程度。应用不可用是可用性问题。锁定的保险库拒绝操作属于信息状态,除非原本预期会有一次已批准的定期检查。保险库报告锁定但金丝雀操作成功,是安全事件。审计链损坏同样是安全事件,即使两个金丝雀目标仍然返回成功。

不要把这些类别隐藏在一个红色或绿色徽章后面。看到 vault=lockedaudit=verifiedhttp=skipped 的操作人员知道该做什么;看到 health=warning 的人只能开始猜测。

最小的实用计划应分成两条通道

频繁运行非秘密检查,然后谨慎、有意地运行需要凭据的金丝雀操作。这两条通道可以减少噪声,并提供更好的证据。

无人值守通道可以在预期 Mac 处于唤醒状态时运行。它检查应用是否存在、保险库锁定时是否拒绝操作、MCP 路径能否返回分类结果,以及 sp audit verify 是否通过。这些检查都不应要求提取凭据或解锁保险库。

凭据通道在人员解锁保险库,并在需要时批准健康客户端会话后运行。它执行一次 HTTP 金丝雀操作和一次 SSH 金丝雀操作,确认远程收据,检查对应的本地活动记录,并再次验证审计链。保持适度的运行频率。每次凭据操作都会生成审计事件和远程服务事件,因此每分钟检查一次只会制造噪声,并不会带来多少额外信心。

在凭据配置、目标别名、代理工具、macOS 权限、网络控制或网关应用发生变更后运行凭据通道。这种时机能够捕捉最可能破坏操作执行的变化,也能为审查人员提供清晰的前后记录。

标准很简单:健康检查套件必须证明你打算依赖的控制措施。如果它只能证明应用图标还在菜单栏中,就把它称为可用性探针,到此为止。不要把它称为自主代理能够安全执行操作的证明。

常见问题

本地代理操作网关的健康检查应该测试什么?

进程检查只能证明某个进程存在。操作网关检查还必须证明保险库正在执行锁定规则、受控的外部操作能够完成,并且该操作会留下可验证的审计记录。

可以在健康检查中使用真实 API 凭据吗?

使用专用的金丝雀凭据,并将其限制在无害的端点或账户上。不要使用生产凭据,也不要让检查打印可能包含秘密的请求标头、命令行、环境变量或响应正文。

保险库锁定时,监控应该报告服务中断吗?

把锁定的保险库视为一种独立且预期中的状态。检查应证明保险库锁定时操作会被拒绝,然后记录授权人员解锁保险库后才运行操作测试。

代理操作网关有哪些安全的 HTTP 健康检查方式?

安全的 HTTP 检查会调用专门设计的端点,由端点验证注入的凭据,并返回固定结果,例如状态码和请求 ID。不要仅仅因为生产 API 的 GET 请求看起来无害,就用它们进行检查。

怎样测试 SSH 执行,又不让代理获得 shell 访问权限?

使用受限的健康检查账户,并在专用 SSH 目标上配置强制命令。强制命令应忽略提交的命令,写入包含服务器时间戳的固定标记,然后返回简短的成功信息。

审计日志足以证明代理操作成功吗?

不能。请求成功并不代表一定留下了记录,审计条目存在也不代表请求一定到达了目标服务。应将操作结果与审计记录关联起来,并单独验证审计链。

怎样避免健康检查把凭据泄露到日志中?

永远不要把秘密放进预期输出文件、测试名称、URL 查询字符串或 shell 参数中。健康检查应比较状态码、不透明的请求 ID、时间戳和固定响应文本等稳定的非秘密证据。

哪些代理操作网关健康检查失败需要告警?

应用不可用、审计完整性损坏,或保险库锁定时网关仍允许操作,都应立即告警。金丝雀目标失败可以降低紧急程度,因为问题可能出在 DNS、本地测试服务或远程服务上。

凭据操作健康检查应该多久运行一次?

频繁运行成本低的可用性探针,但降低凭据操作检查的频率,并在配置变更后运行。每次操作检查都会写入审计事件,过于激进的间隔会制造噪声并增加调查难度。

Mac 离线时,这些检查还能运行吗?

即使 Mac 离线,也可以证明应用可用性、本地保险库行为、本地审计完整性和 MCP 连通性。但在 Mac 能够访问受控目标之前,无法证明远程 HTTP 或 SSH 操作。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov