# 单进程与基于守护进程的代理安全工具

一张进程图可能让安全产品看起来简单得令人放心，也可能显得复杂而严谨。但这两种观感都不能告诉你：自主编程代理是否会滥用凭据、连接到不该连接的本地服务，或者在崩溃后留下未记录的操作。

真正有用的比较应当具体：数清信任边界，检查跨越这些边界的接口，再跟踪一个秘密从存储到出站请求的完整路径。单应用进程可以减少本地组件。守护进程可以隔离权限，或让服务在多个客户端之间持续运行。两种设计都会在把进程分离误当成授权时失败。

## 进程数量并不决定安全边界

单进程设计将用户界面、凭据库、批准状态、请求验证、网络操作和日志记录放在同一个地址空间中。各部分之间没有本地客户端套接字，也不需要维护独立的服务注册，更没有某个本地进程用来控制另一个本地进程的协议。这确实可以减少攻击面。

但它也会形成一个很大的信任域。如果攻击者在该进程中取得代码执行能力，就可能触及该进程能够访问的每个组件。内存破坏、恶意插件加载、不安全的更新行为，或过于宽泛的脚本接口，都会变得尤其危险，因为凭据库和执行器与普通应用代码处在同一个环境中。

基于守护进程的设计会拆分这些职责。桌面应用或命令行客户端向后台服务发送请求。服务可能负责持有秘密并发起网络调用，客户端则显示批准界面并接收结果。这可以限制被攻陷客户端的能力，但前提是守护进程必须拒绝客户端无权发出的请求。

架构讨论中经常跳过这一点。限制给单个用户账户的 Unix 域套接字，只代表传输层可以访问，并不能证明调用者就是目标代理或获准的应用。该用户运行的每个进程都可能打开它。如果守护进程接受任何本地对端发来的「使用凭据 X 访问 URL Y」请求，那么恶意编辑器扩展、Shell 脚本或被攻陷的代理都可以要求执行同样的操作。

请区分以下三个概念：

- 进程边界隔离内存。
- 权限边界限制代码在被攻陷后能做什么。
- 授权边界决定哪个调用者可以请求哪些操作。

团队经常把第一个概念当成另外两个概念的证明，但事实并非如此。两个以同一用户身份运行、通过未认证 IPC 通信的进程虽然拥有独立的堆，但仍可能处在同一个授权域中。

单进程可以避开 IPC 授权问题，因为其内部调用不会跨越本地服务边界。但这并不意味着你不需要在批准操作前识别外部代理会话。守护进程会把这个识别问题增加一遍，客户端端点需要识别，任何暴露的管理端点通常也需要再次识别。

## 本地套接字也是恶意代码可以调用的 API

守护进程的支持者经常正确地指出，服务只监听 localhost 或 Unix 套接字。被省略的是，本地代码正是大多数代理集成运行的地方。编程代理、终端、编辑器、构建脚本、软件包钩子和浏览器辅助程序都共享同一台机器。

应该像对待小型网络 API 一样对待本地 IPC 端点。严格定义消息格式。在协议允许的情况下拒绝未知字段。将每个请求绑定到调用者身份和短期会话。限制请求大小、并发数量和目标范围。拒绝请求也要记录，不能只记录成功请求，因为反复被拒绝可能说明某个集成正在尝试绕过预期路径。

在 macOS 上，在相信某个工具「仅限本地运行」之前先检查它：

```sh
ps -axo pid,ppid,user,command | grep -i '[a]gent\\|[d]aemon'
lsof -nP -iTCP -sTCP:LISTEN
launchctl print gui/$(id -u) 2>/dev/null | grep -i -C 2 'agent\\|vault\\|security'
```

当某个进程监听 TCP 时，第二条命令通常会输出类似这样的列：

```text
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
service  4128 sam     9u  IPv4 0x...            0t0  TCP 127.0.0.1:48120 (LISTEN)
```

`lsof` 没有输出，并不能证明工具没有 IPC。Unix 套接字不会出现在这个 TCP 查询中。检查应用支持目录、临时目录和服务文档，查找套接字路径。然后使用 `ls -l` 检查权限，并思考同一账户拥有的其他进程是否可以连接。

Apple 的 `launchd.plist` 手册将 `KeepAlive` 描述为 launchd 重启任务的一组条件。这在运维上很有用，但也带来安全责任。服务重启后，必须正确恢复锁定状态、会话状态和审计状态。「它会自动恢复」不能回答重启后的第一秒内它会接受什么请求。

优秀的守护进程会把调用者身份作为协议的一部分，而不是把它当成客户端提供的假设。具体机制取决于操作系统。在能够为本地套接字提供对端凭据的平台上，应使用这些凭据。在可以获取代码签名信息的地方，应在授予会话前进行验证。不要把进程名称、JSON 中由调用者提供的 PID，或调用者填写的路径当作身份。这三者都很容易伪造，或者等你检查时已经过期。

## 启动行为决定保护是否会在需要时存在

单应用进程通常在用户打开应用时启动。敏感状态的生命周期很直观：应用启动，用户解锁，获准的会话运行，应用关闭或崩溃后访问结束。这种行为容易解释，也容易测试。

代价是可用性。应用关闭、仍在启动、处于锁定状态，或被系统权限提示阻塞时，命令行代理无法使用网关。对于由人控制的凭据网关，这可能正是正确行为。如果预期任务需要在重启后无人值守地运行，而产品又没有安全恢复权限的方式，这种设计就不合适。

守护进程通常在登录、按需或系统启动时运行。每种选择都会改变威胁模型。登录时启动的服务可能要等待用户的密钥存储和桌面会话。系统启动时运行的服务可能会在用户能够批准任何操作前就开始工作。按需服务可以减少空闲暴露，但首次请求不能与初始化发生竞争。

我见过这种糟糕的失败方式：界面启动很慢，后台服务已经开始接受请求，而服务把「尚未加载批准状态」当成「不需要批准」。开发者写这个分支是为了避免启动错误。攻击者和不稳定的自动化流程不会在意它为什么存在。

把状态机写下来。它应该明确回答以下问题：

1. 凭据库报告已锁定或已解锁之前，执行器能否接受请求？
2. 与客户端进程绑定的批准，在该进程退出后会怎样？
3. 服务在请求进行期间重启，会怎么处理请求？
4. 待处理的用户提示能否在重启后继续，并绑定到另一个调用者？
5. 注销是否会撤销服务可使用的凭据状态？

对于高风险操作，在每个不明确的状态转换期间都应默认拒绝。服务初始化时收到的请求应该得到明确的拒绝，或可重试的不可用响应。它不应继承旧批准、缓存的已解密凭据或默认允许决定。

启动行为还会影响用户习惯。如果代理工作流因为用户必须手动恢复一个隐藏服务而中断，人们就会关闭这个安全门，或把令牌存进环境变量。无法应对普通睡眠、注销和重启的安全控制，实际上是在训练用户绕过它们。

## 凭据必须直接到达执行器，不能经过代理

核心问题不在于工具是否加密凭据库，而在于代理是否曾经以可以复制、打印、写入文件或发送到其他端点的形式接触过秘密。

最安全的安排是让网关控制的凭据库存放秘密。代理提交操作请求。网关检查授权状态，将凭据注入出站 HTTP 请求或 SSH 操作，执行操作，然后返回结果。代理收到的是远程系统产生的数据，而不是 bearer 令牌或私钥。

这一区别很重要，因为代理处理的是文本。如果令牌出现在工具输出中，模型可能会把它回显到 Shell 命令、源文件、问题描述、构建日志或聊天记录中。之后再遮盖显示字段并不能修复暴露，秘密已经跨过边界。

OWASP 的 Secrets Management Cheat Sheet 建议团队避免硬编码秘密，并在怀疑暴露时轮换秘密。这一建议是正确的，但对代理工作流来说还不完整。秘密即使没有进入源代码管理，也可能通过工具响应泄露。网关必须在执行操作的那一刻阻止披露。

当守护进程负责解密和出站网络连接时，它可以很好地保护这条边界。客户端发送操作描述，而不是请求原始秘密。守护进程还应避免返回过于宽泛的诊断信息。失败的身份验证响应可以告诉调用者请求失败，但不需要返回注入的授权请求头、序列化后的 SSH 配置，或凭据提供程序的内存转储。

单进程也可以遵循同样的原则。内部执行器读取凭据库并发起调用，代理则通过受限制的集成点进行通信。它的好处是无需第二个进程接触已解密的材料。风险在于应用中的普通功能可能与秘密处于同一个内存域。

避免以下看似方便的安排：

- 将 API 令牌放进代理进程的环境变量。
- 写入临时凭据文件，再让子进程读取。
- 返回一个占位符，由客户端稍后兑换真实令牌。
- 为了方便，让本地守护进程暴露「获取秘密」方法。
- 通过标准输入传递 SSH 私钥字节。

占位符模式尤其值得怀疑。只有当占位符不会在网关之外授予凭据访问权，也不能被另一个进程重放时，它才是安全的。实际上，团队经常把它变成未记录的 bearer 令牌，于是又构造出了一个生命周期控制更弱的第二凭据。

SSH 也要遵循同样的纪律。辅助程序可能需要使用密钥建立连接，但代理应该请求它执行特定的远程命令，或执行范围严格受限的连接操作。因为代理「只需要短暂使用」，就把私钥交给它，仍然等于把私钥交给了它。

## 更多进程意味着更多维护工作，不意味着更成熟

守护进程会带来单进程桌面应用可以避开的运维工作。有人必须安装它，在正确的用户或系统上下文中启动它，更新它，验证其二进制文件，处理崩溃，清理过期注册项，并确保升级后套接字和日志文件的所有权正确。

这不是反对守护进程，而是反对假装这些工作会自动消失在操作系统中。`launchd` 可以重启任务，却无法判断新二进制文件是否保留了预期的调用者检查，旧套接字是否在升级后残留，或服务是否开始使用不同的权限集合。

守护进程还会让版本匹配变复杂。命令行包装器可能使用协议版本 A，而已安装的服务需要版本 B。如果处理不当，客户端可能退回到未经认证的兼容路径，或为了保持流畅体验而关闭检查。兼容代码造成的问题往往超出团队预期，因为它恰恰在系统最不明确时运行。

对于不兼容的版本，应明确拒绝。错误信息应该告诉用户更新哪一方，而不是默默协商到更弱的模式。协议应保持足够小，使你能够测试旧客户端、服务重启、格式错误的消息和并发请求，而不必围绕它搭建完整的测试实验室。

单进程应用同样需要更新纪律。签名应用可以改变自身实现和凭据库架构。区别在于范围更小：只有一个可执行文件生命周期需要检查，而且展示批准界面的进程与执行受保护操作的进程相同。更新仍可能引入缺陷，但不会自动引入本地 RPC 协议和服务管理器状态。

对团队来说，这种权衡更明显。中央守护进程可以为多个工具提供稳定的本地端点，从而减少重复集成。但它也可能成为共享故障点。一个卡住的服务就能阻塞机器上的所有开发工作流。一个权限过大的服务可以让每个本地客户端访问每个已配置的凭据。

不要把后台菜单栏图标当成守护进程健康的证明。测试更棘手的情况：强制退出界面、终止服务、在活跃会话期间重启、先升级客户端、先升级服务，以及在请求等待批准时删除凭据。如果任何一种情况的答案是「应该可以恢复」，你其实还不了解它的运行行为。

## 批准应绑定到已识别的运行，而不是一台被记住的机器

当批准的是「这台电脑」或「当前用户」这类模糊概念时，批准系统就会失败。自主代理经常生成子进程，在编辑后重启，并通过 Shell 调用工具。能够跟随所有这些活动的权限，通常已经过于宽泛。

将批准绑定到特定的进程运行，或绑定到无法被无关本地代码复制的其他身份。批准界面应显示足够的来源信息，让人能够发现意外情况：可执行文件的签名方、命令路径和请求类别，都比单独显示一个友好的客户端名称更有证据价值。任何人都可以选择一个进程名称。

然后明确批准的持续时间。对于低风险的重复调用，一次短代理会话批准可能合理。敏感凭据可能需要每次都征得同意。撤销必须立即停止已识别的运行，而不能只是在控制面板中隐藏它，同时让本地连接继续保持。

守护进程在这里还有额外负担。它必须将 IPC 连接映射到用户批准的身份，并在客户端退出或重新连接时丢弃这个映射。不能让客户端仅凭展示缓存的标识符就恢复旧会话。如果标识符可以通过文件或命令参数传递，它就是一个换了好听名字的 bearer 凭据。

单进程设计可以将批准状态放在活动集成旁边，从而减少映射工作。但它仍然需要区分一次代理启动和另一次启动。如果应用无法判断请求来自已批准的运行，还是来自之后替换它的进程，就应该再次提示。

实际需要的策略通常比团队想象的简单：凭据库锁定时拒绝，新识别的运行首次请求时询问，对于值得这份额外摩擦的凭据要求单次确认。复杂的本地策略语言往往会产生没人能在压力下审计的规则。用户能够预测的短决策路径更容易测试，也更难被意外绕过。

## 重启失败会暴露最薄弱的连接处

设想一名开发者批准代理在一个活跃的终端运行期间执行普通 HTTP 调用。网关将批准存放在内存中。代理提交请求，网关开始准备出站调用。就在此时，界面崩溃，或后台执行器在更新后重启。

粗心的守护进程实现可能通过几种方式让情况变得更糟。执行器在加载凭据库锁定状态前就返回。客户端套接字以相同路径重新出现。代理重新连接。守护进程在请求中看到缓存的会话标识符，就认为旧批准仍然有效。同时，审计写入器尚未重新打开日志，因此成功的重试没有持久记录。

每个单独的选择听起来都很方便：保留会话、减少提示、快速重启、缓冲日志。但它们放在一起，就会在系统状态最不可靠的时刻产生一次未批准的操作。

安全行为其实更简单。重启时丢弃易失的批准。在明确保护状态之前，让凭据库保持不可用。在审计写入器准备好提交记录之前，拒绝新操作。让代理收到拒绝或暂时不可用响应；如果会话已不存在，就要求重新批准。

无需特殊测试硬件也可以执行这项测试：

1. 打开新的代理会话，并批准一个允许的低影响请求。
2. 启动另一个在批准点或测试端点等待的请求。
3. 请求等待期间终止安全进程或服务。
4. 重启它，并从原代理进程重试请求。
5. 验证它需要新的授权，并且最终结果只在审计记录中出现一次。

使用客户端重新连接、设备锁定、删除凭据和升级等情况重复测试。你要寻找的是过期权限、重复执行或缺失记录。在事件发生时，如果工具无法让这些结果清晰可见，就很难信任它。

## 审计记录需要一个无法绕过的写入器

安全日志经常记录用户界面看到的内容，而不是凭据执行器实际执行的内容。当守护进程直接接受本地请求时，这个差距会变得严重。被攻陷的客户端可能跳过界面，或者界面可能在执行器完成操作后才崩溃。

将审计写入放在操作路径上。发送 HTTP 请求或调用 SSH 辅助程序的组件，应记录调用者身份、凭据引用、目标、请求类别、决定和结果。默认不要记录原始秘密或请求正文。包含 bearer 令牌的日志会变成访问控制更差的另一个凭据库。

单进程可以直接安排顺序：验证，记录决定，执行，再记录结果。守护进程必须确保自己的执行器无法通过绕过日志组件的旁路执行请求。只有在能够接受失败模式并证明顺序正确时，才应通过 IPC 将日志和执行分开。

篡改证据不仅在日常排查中有用，在遭到攻陷后更重要。如果攻击者可以编辑本地 SQLite 文件，或从文本日志中删除指定行，那么这份记录可能有助于调试，却不能建立可信的历史。采用面向追加的记录并进行密码学链接，可以让调查人员发现篡改，前提是他们保留了所需文件并对其进行验证。

Sallyport 从一个加密、哈希链式的审计日志中生成会话和调用视图，`sp audit verify` 命令可以在离线状态下检查链条，不需要凭据库密钥。这解决了独立的界面日志和守护进程日志经常造成的问题：两份记录对某项操作是否发生给出不同结论。

不要夸大审计的能力。哈希链无法恢复攻击者阻止程序写入的事件，只能检测它收到的记录集合中的改动。但这仍然有用，尤其是在崩溃、升级或意外的本地客户端发生后，你需要回溯整个过程时。

## 选择满足所需生命周期的最小设计

当受保护的工作需要人在场、代理运行期间应用可以保持打开，并且减少本地端点比独立于界面运行更重要时，选择单进程。这种布局适合凭据网关，其主要承诺是让人看到并控制每次代理运行。

当任务确实需要服务持续运行、与信任程度较低的客户端隔离，或需要独立的操作系统权限时，选择守护进程。对它增加的每个接口都要求明确回答：谁可以连接，服务如何识别调用者，他们可以发出什么请求，状态何时过期，以及任意一方重启后会发生什么。

Sallyport 这样的桌面工具采用单应用进程路径，同时使用常规的 MCP shim 接收代理请求。它的凭据库自行执行 HTTP 或 SSH 操作，因此代理永远不会收到已配置的 API 或 SSH 凭据。

采用任一模型前，先进行一次不带感情色彩的检查。代理工作期间，终止持有权限的组件，重启它，然后检查下一个请求是否被拒绝、重新批准并记录。这个测试比任何精美的架构图都更能揭示设计的真实情况。
