# AI 智能体的 sudo 访问权限：控制每一次提权

让 AI 智能体使用 sudo，只因为它已经能够打开远程 shell，这是把两个不同问题混为一谈。远程执行回答的是「它能连接这台机器吗？」提权回答的是「这个请求可以修改受保护的状态吗？」这两项决定需要不同的证据、控制措施和记录。

我见过团队把它们合并成一个便利功能：智能体通过 SSH 连接，运行 `sudo`，最后只留下一份聊天转录。看起来很整洁，直到一条错误命令重启了错误的服务、替换了配置文件，或者遵循了仓库中隐藏的指令。此时，没有人能说清楚哪个进程持有权限、谁批准了操作，也没人知道为什么需要 root 访问权限。

**AI 智能体的 sudo 访问权限**应当表示：针对已说明操作的一次范围有限、可追责的例外。它绝不能表示智能体在整个任务期间获得了管理员的常驻权限。

## 远程 shell 和 sudo 回答的是不同问题

SSH 会话只能证明某个客户端已向远程账户完成身份验证。它不能证明该会话中输入或生成的每条命令都值得获得 root 权限。应让普通账户足以完成发现、构建、测试、读取日志和准备部署等工作，再把少量受保护的变更放到独立的提权路径后面。

shell 会让权限提升看起来像一个语法细节，因此这种区别很容易被忽略：

```sh
ssh deploy@api-02 'sudo systemctl restart payment-worker'
```

这一行至少隐藏了五个决定：是哪一个智能体进程发出了命令，目标主机是哪一台，服务名称是否正确，为什么需要重启，以及是否有人接受了后果。SSH 可以验证连接身份，`sudo` 可以切换有效用户，但这两个工具本身都不会记录原因，也不会提供适合自主工作的决策点。

普通远程命令应继续保持普通。智能体可以运行 `systemctl status`，查看自己已有权限读取的服务日志，比较渲染后的配置，或执行健康检查，而不必请求 root。这样做还有实际好处：智能体可以先收集证据，再请求批准变更。

不要走向另一个极端，把每条 shell 命令都放在人类确认之后。那会造成批准疲劳，人们会开始点击那些已经不再阅读的卡片。应把阻力放在会改变受保护边界的操作上，例如服务管理、系统软件包变更、特权文件写入、账户变更、网络规则、机密材料、启动设置和生产数据操作。

命令的写法不能决定它的风险。`sudo cat /var/log/...` 可能暴露敏感内容。`sudo systemctl restart ...` 可能中断面向客户的服务。看似无害的 `sudo install` 也可能替换可执行文件。应根据操作影响、目标，以及参数是否能逃逸为任意 root 行为来分类。

## sudoers 规则是允许列表，不是安全论证

`sudoers` 手册说明，sudo 会根据命令路径，以及在配置启用时的命令行参数，判断用户是否可以运行某条命令。这是有用的机制，但它不会把宽泛的允许列表变成安全委托。

例如，下面的规则授权范围远超许多人的预期：

```sudoers
autobot ALL=(root) NOPASSWD: /usr/bin/systemctl *
```

它允许账户启动、停止、重启、启用、禁用、屏蔽以及检查本地 `systemctl` 支持的任何单元。如果智能体能够影响单元文件、环境文件或服务可执行文件，就可能把一次获准的重启变成以 root 身份执行代码。该规则也没有记录变更理由或过期时间的地方。

参数限制只有在允许的程序拥有小而稳定的接口，并且不会把攻击者控制的输入解释为路径、shell 表达式、插件、编辑器、分页器或配置源时才真正有帮助。管理员经常忽略这一点，因为命令看起来很熟悉。可执行文件路径使用绝对路径，并不会自动让命令变得范围有限。

更好的做法是使用专门设计的 wrapper，并固定操作、明确校验。例如，重启 wrapper 可以接受硬编码的服务名称，而不是任意 `systemctl` 参数：

```sh
#!/bin/sh
set -eu

case "${1:-}" in
  payment-worker|report-worker) ;;
  *) echo "unsupported service" >&2; exit 64 ;;
esac

exec /usr/bin/systemctl restart "$1"
```

然后将 sudo 限制到该 wrapper，并在平台支持时限制为精确参数：

```sudoers
autobot ALL=(root) /usr/local/sbin/restart-approved-service payment-worker, \
                    /usr/local/sbin/restart-approved-service report-worker
```

这样可以避免 `systemctl` 子命令不断扩张。但它并不能回答此刻重启是否合理。wrapper 还需要由 root 所有，父目录不能被写入，并且要防止智能体控制的环境变量影响它。如果智能体可以编辑 wrapper，或替换它执行的某个文件，这条规则就已经失效。

不要采用「自动化就直接使用 NOPASSWD」这种流行建议。人们喜欢它，是因为无人值守任务不会再因密码提示而失败。但对于自主智能体来说，密码提示从来不是有意义的控制。用不受限制的提权取代它，只是移除了最后一个可见的停顿。应以明确授权和可追责记录取代它，而不是追求空洞的便利。

## 提权请求需要一个审核者能够判断的理由

理由是授权决策的一部分，不是事后复制到工单里的装饰性句子。智能体应在提权前创建请求，批准界面则应把拟执行的操作和理由一起展示。

请求至少应绑定以下字段：

- 精确目标，例如 `api-02` 和 `payment-worker`。
- 请求执行的特权操作，包括固定参数。
- 与事件、部署、维护任务或观察结果相关的理由。
- 预期影响和回滚操作。
- 足够短的过期时间，避免被遗弃的请求变成常驻权限。

理由必须包含人类能够评估的证据。「需要 sudo 修复构建」说明智能体还没有完成足够的诊断。「当前证书续期报告解析错误，需要用审核过的发布配置替换 `/etc/acme/client.conf`；如果验证失败则恢复旧版本」则指出了文件、触发条件和恢复路径。

即使保留自由文本说明，也应使用结构化请求。结构化字段能防止智能体在计划阶段和执行阶段之间悄悄改变目标，也让后续审核不必从多个聊天窗口中重建一次决定。

使用不可变的请求标识符。批准应授权该标识符、指定目标和指定操作。不要批准「处理这次故障」这样的自然语言指令，再让智能体稍后自行决定哪些 root 命令属于该指令。这会把人类批准变成一张无限额度的空白支票。

一个有用的请求记录可以如下所示：

```json
{
  "request_id": "elev-7f4c2",
  "agent_session": "run-91b0",
  "host": "api-02",
  "operation": "/usr/local/sbin/restart-approved-service payment-worker",
  "reason": "Deployment 482 left payment-worker unhealthy; status shows repeated configuration parse errors.",
  "expected_effect": "Service restarts with the reviewed configuration.",
  "rollback": "Restore the previous configuration revision and restart the service.",
  "expires_at": "2025-03-08T14:25:00Z"
}
```

执行记录也应携带 `request_id`。没有这个绑定，批准者可能批准了一项操作，而智能体执行了另一项。

## 智能体进程需要自己的身份

共享的 `deploy` 账户会让所有执行者在事故发生后看起来完全相同。应为每次智能体运行分配身份，记录启动它的可执行文件、发起它的人或服务、收到的仓库和任务，以及权限何时结束。

人类身份和进程身份是两件不同的事实。开发者可能启动了智能体，但智能体进程可能在数小时后，读过文件、工具输出和网络响应后才发出命令。审计轨迹应保留这两项事实。「Jamie 发起了运行 91b0」和「经过签名的智能体进程 91b0 请求提权」能让调查者区分发起责任和执行责任。

代码签名有助于识别提出访问请求的本地可执行文件，但它不能证明模型的指令是安全的，也不能让受感染的仓库变得可信。应把进程身份当作请求者范围的约束，而不是请求明智与否的证明。

不要让智能体获得可重复使用的 root 密码、直接落到管理员账户上的私有 SSH 凭据，或能够无限刷新、长期存在的 sudo 时间戳。每一种做法都会把范围有限的请求变成可携带的能力。一旦智能体可以把这种能力复制到工作区、日志、构建产物或子进程中，你就失去了对其扩散的控制。

短期凭据可以减少暴露时间，但不会让错误命令变正确。应将过期时间与明确定义的操作、绑定的进程和理由一起使用。缺少其中任何一项，得到的仍然只是换了一个好听名称的访问令牌。

## root shell 给了智能体太多重新解释指令的空间

绝不要为智能体会话批准 `sudo -i`、`sudo su`、`sudo sh` 或不受限制的 `sudo bash`。root shell 会授权之后的每一条命令，包括从工具输出中拼出的命令，而那些输出可能在最初批准第一条命令时还不存在。

同样要警惕伪装成任意解释器的命令形式：

```sh
sudo python3 -c "$AGENT_TEXT"
sudo env CONFIG="$AGENT_TEXT" /usr/local/sbin/apply-config
sudo tee /etc/some-file.conf
```

这些例子在检查输入渠道之前看起来都很有限。Python 可以执行任意代码。`env` 可以以调用者未预料的方式改变程序行为。只要路径没有受到约束，`tee` 就能让调用者把写入权限扩展到任意 root 所有路径。忽略参数和环境的命令策略，本质上只是文件名策略。

更好的设计是把诊断和执行分开。智能体可以检查非特权事实，生成拟议变更，再提交审核。狭窄的特权 helper 只接收经过批准的输入。helper 仍需再次验证这些输入，因为审核时验证和执行时验证分别防范不同的失败。

假设部署智能体发现服务故障。在宽泛的 sudo 下，它可能编辑单元覆盖配置、重新加载管理器并重启服务。仓库控制的配置中若有恶意字符串，就可能变成 `ExecStart` 行，并以 root 权限执行。受约束的设计则允许智能体读取状态并准备经过审核的配置，但特权部署 helper 只接受来自已批准产物位置的内容摘要。它会拒绝单元文件、任意路径和未托管的覆盖配置。

这比交出一个 shell 更费事，但这也是已知操作与批准后仍能自行发明新操作的解释器之间的区别。

## 批准应匹配操作的风险

对于日常且受限的远程操作，一次会话批准可能合理。但它不适合管理员能力的每次使用。应将会话授权和单次调用授权视为两个独立控制。

会话授权回答：「这个已识别的智能体进程，在本次运行期间可以使用普通远程通道吗？」它能防止未知的本地进程悄悄冒充已知智能体。进程退出时授权应结束，操作员还应能够立即撤销它。

单次调用授权回答：「现在可以运行这个具体操作吗？」对于影响范围大、凭据稀缺、会影响生产环境或无法逆转的操作，应使用单次批准。批准卡片应首先显示进程身份，然后展示目标、操作、理由、过期时间和预期影响。把身份埋在大段文字下面，会削弱这项控制的意义。

不要让操作员解析一百行生成的 shell。应给智能体一组有名称的操作，并渲染清晰的细节。「在 api-02 上重启支付工作进程」可以审核；带有嵌套引号的 shell 代码块会让人只能对自己无法理解的内容盖章。

批准流程也需要一条能帮助智能体恢复的拒绝路径。返回清晰的拒绝信息，并在适当时说明「需要部署变更记录」或「生产环境不可执行此操作」。不要让智能体通过稍微改变措辞不断重试，直到疲惫的人接受请求。拒绝应关闭该请求，除非人类创建了一个包含实质性不同信息的新请求。

紧急情况也应保持同样的纪律。值班工程师可以批准一个过期时间很短、带有事件引用的狭窄请求。紧迫性可以让审核更快，但不能成为删除记录或授予交互式 root shell 的理由。

## 审计记录必须在创建请求的智能体退出后仍然存在

终端转录有助于调试，但不能承担审计记录的全部责任。智能体可以省略它、修改本地文件，或通过转录未捕获的路径运行命令。授权和执行都应记录到智能体无法改写的存储中。

将决定与行动分开记录。批准记录应显示批准人、批准时间、进程身份、完整请求内容和过期时间。执行记录应显示 helper 是否运行、访问了哪台主机、返回了什么、何时结束，以及授权它的请求标识符。

记录结果摘要或受限的输出摘要，不要把敏感命令输出全部倒入广泛可见的日志。调查者需要足够证据确认 `payment-worker` 已重启并恢复健康，但不需要一并看到恰好出现在 stderr 中的数据库密码。

由于审计日志在事故后才真正受到关注，防篡改证据很重要。哈希链将每条记录与前一条记录关联起来。有人修改或删除记录时，验证就会发现链条中断。验证应独立于智能体，并尽可能独立于执行操作所使用的凭据。若验证日志必须使用已被攻破的管理员凭据，那么在最需要它时，这份日志就没那么有用了。

例如，离线验证器应报告类似下面的序列：

```text
$ sp audit verify --file agent-audit.enc
verified records: 184
chain start: 8c1a...e72d
chain end: 4bf0...193a
status: valid
```

重要的不是命令名称，而是验证器能够在不要求智能体自我解释的情况下发现被修改的密文记录。应在智能体工作的机器之外保存副本，因为控制该机器的攻击者可能会删除整个日志。

Sallyport 采用了这种分离方式：凭据不进入智能体，并通过一个不可写的加密哈希链审计日志，分别呈现会话日志和单次操作日志。当智能体需要执行 HTTP 或 SSH 操作，却不应持有授权凭据时，这种模型很有价值。

## 特权 helper 需要狭窄接口和恶意输入测试

即使 helper 只有二十行代码，也属于安全敏感代码。应假定每个参数、环境变量、工作目录和引用文件都来自攻击者，因为 AI 智能体可能在无意造成伤害的情况下被诱导传入恶意内容。

先建立操作清单。写下智能体真正需要的每一种特权影响，例如重启指定服务或安装经过签名的发布产物。如果无法描述某个影响，而只能说「运行任意命令」，说明该操作尚未完成设计。

将 helper 放入 sudoers 前，逐项回答：

1. 调用者可以提供哪些确切输入，helper 如何分别验证它们？
2. 哪些文件系统路径、可执行文件、配置文件和环境变量会影响其行为？
3. 任何已接受的输入能否触发 shell、解释器、分页器、编辑器、插件加载器或网络请求？
4. helper 是否会在执行前验证所有权、权限和内容身份？
5. 验证失败和执行成功时，它分别会生成什么记录？

不要只测试成功路径，也要运行否定测试。传入 `../` 路径遍历、shell 元字符、空服务名、超大输入、意外 Unicode，以及一个格式有效但指向不安全状态的名称。尝试在验证和使用之间替换被引用的文件。检查可写日志目录、临时目录或父目录是否能让智能体重定向 root 输出。

更新后也要检查依赖。某个 helper 可能在某版本命令下安全，但新版本可能增加接受配置路径的选项，或加载扩展。狭窄接口能减轻维护负担，却不能完全消除风险。

## 将提权作为受控例外逐步推出

先移除最危险的常驻权限形式：共享管理员账户、不受限制的 `NOPASSWD`、智能体配置中的可重复使用 root 密钥，以及 root shell。你不必在完成这项改变前停止所有自动化。先保留只读诊断，再一次将一个受保护操作迁移到明确的 helper 和批准流程中。

选择一个足够常用、便于验证，同时回滚范围明确的操作，例如在经过审核的部署后重启指定工作进程。要求智能体提交主机、理由、影响、回滚方式和过期时间，并让批准人拒绝含糊的请求。这种阻力会教会智能体：在请求权限之前，必须先收集哪些证据。

对被拒绝的请求也要像对成功请求一样认真复盘。大量任意文件写入请求，可能说明部署接口缺少必要操作，也可能说明智能体不断尝试绕过边界。这是两个不同的问题，审计轨迹能帮助你区分它们。

随后练习撤销。智能体工作时终止会话，在请求过期后拒绝已经发出的请求，并验证复制出来的请求标识符无法授权第二次操作。团队经常测试批准界面，却跳过这一步。智能体在运行中途开始表现异常时，你真正需要的就是撤销能力。

不要用智能体能以 root 身份运行多少条命令来衡量成熟度。应看每次提权是否始终范围有限、可追责、有时间限制、可审核，并且在智能体或其输入出错时能够恢复。
