# 为 AI 编程代理安全设置独立 Unix 账户

把自己的 Unix 登录账户交给 AI 编程代理，是一条后患很长的捷径。代理会继承那些你早已忘记的文件、工具多年前缓存的凭据、宽松的 SSH 配置、部署脚本，还能让一次糟糕的修改看起来完全像是你的日常操作。专用远程账户不会让代理变得无害，但能让它拥有的权限清晰可见，也更容易限制。

把账户当作特定任务的边界。如果代理只需要编辑一个代码库、运行测试并推送分支，就创建一个能完成这些工作的身份。不要先使用开发者账户，再试图事后删减权限。Unix 权限会通过用户组、挂载目录、Shell 配置以及默认假设有人负责的工具不断累积。

## 独立 Unix 账户会给代理一个不同的身份

专用账户会拥有独立的 UID、进程所有者、主目录、SSH 授权集合和审计记录。这五点比在提示词中要求代理不要离开某个代码库重要得多。

当代理以 `alex` 身份运行时，它启动的每个进程都属于 `alex`。这些进程可以读取 `alex` 能读取的所有内容。如果转发功能或套接字暴露了 SSH agent，它还可以使用 `alex` 的 SSH agent。它可以查看 Shell 历史、Git 凭据、云配置、包管理器缓存，以及 `alex` 拥有或能够读取的项目目录。即使代理今天表现完美，你也已经让它未来能拥有的权限取决于你今后给自己账户添加的每一种便利设置。

像 `agentbuild` 这样的账户让起点变得可检查：

```sh
id agentbuild
getent passwd agentbuild
sudo -u agentbuild sh -lc 'umask; pwd; env | sort'
```

在典型的 Linux 主机上，第一个命令应显示一个 UID 和一份很短的组列表。第二个命令应显示类似 `/srv/agentbuild` 或 `/home/agentbuild` 的主目录，而不是开发者个人的主目录。最后一个命令可以发现一个容易被忽略的问题：继承的环境变量可能指向凭据文件、代理服务器、令牌缓存或异常的可执行文件路径。

这里经常有人混淆两件事：独立的 SSH 密钥不等于独立账户。让一把新密钥登录现有账户，只改变了身份验证方式。登录之后可访问的文件、命令和网络配置并没有减少。独立身份验证和独立授权解决的是不同问题。

让稳定的账户对应稳定的职能。如果一个代理负责构建拉取请求，另一个代理负责部署发布产物，就给它们不同的身份。这样，当你需要回答一个令人不安的运维问题时，不必靠猜：究竟是哪个账户修改了这个文件、打开了这个网络连接或创建了这个进程？

## 账户边界无法限制所有类型的损害

Unix 账户能限制由 Unix 所有权、组、访问控制列表和权限控制的访问。但它不会自动限制网络目标、CPU 使用量、磁盘耗尽、内核漏洞、全局可读文件，或共享服务凭据授予的访问权。

这个边界仍然非常有价值。编程代理通常需要修改源代码、运行包脚本、读取配置和使用 Git。包脚本可以执行任意 Shell 命令，构建系统可能读取环境变量，被攻破的依赖也能这样做。应当假设：主机要求运行的任何代码，都将获得运行该代码的账户所拥有的权限。

不要把账户隔离误认为容器、虚拟机或网络防火墙。它们承担不同的工作：

- Unix 账户分隔本地文件、进程所有权和普通命令权限。
- 容器可以限制文件系统视图和资源使用，但错误挂载的主机目录会破坏这项好处。
- 当工作负载需要时，虚拟机可以提供更强的操作系统边界。
- 网络控制决定账户可以连接到哪里，以及哪些内容可以离开主机。

选择与失败后果相匹配的最小组合。对于一次性测试代码库，隔离工作节点上的专用账户可能已经足够。对于生产部署主机，应在账户边界之外使用受限 SSH、范围狭窄的部署凭据、日志和出站策略。如果代理可以访问生产数据库或签名材料，仅有独立 UID 显然不够。

有一种流行但错误的建议：创建一个账户，并把它加入与开发者相同的运维组，以便构建顺利。这只是在换了用户名的情况下重现原来的问题。`docker`、`libvirt`、备份组、设备组和特权日志组等组，可能带来远超其名称所暗示的权限。在许多系统中，加入 `docker` 实际上就意味着可以控制主机，因为组成员能够启动一个挂载主机文件系统的容器。

## 创建一条没有继承痕迹的账户链

创建远程账户时，不要启用密码登录，不要加入管理员组，并让主目录只包含你明确放入其中的文件。从空目录开始，因为复制点文件是意外获得权限的常见来源。

具体命令因操作系统而异。在 Debian 或 Ubuntu 风格的主机上，管理员可以这样创建一个带专用主目录的本地账户：

```sh
sudo adduser --disabled-password --gecos '' --home /srv/agentbuild agentbuild
sudo passwd -l agentbuild
sudo install -d -m 700 -o agentbuild -g agentbuild /srv/agentbuild/.ssh
sudo -u agentbuild touch /srv/agentbuild/.hushlogin
```

`--disabled-password` 会阻止该账户通过普通密码进行身份验证。`passwd -l` 会在支持锁定密码的系统上明确表达这一意图。但不要把这两项设置当作唯一的 SSH 控制手段：SSH 服务器有自己的密码身份验证设置，如果你安装了密钥，现有密钥仍然可以完成身份验证。

在支持 `useradd` 的系统上，使用能创建主目录和私有组的选项，然后验证结果，不要只凭记忆。BSD 和 macOS 使用不同的账户管理工具，因此应查阅本地的 `dscl`、`sysadminctl` 或系统管理手册，不要把 Linux 命令直接粘贴到另一种主机上。

立即检查所有权：

```sh
namei -l /srv/agentbuild/.ssh
sudo -u agentbuild sh -lc 'touch ~/permission-test && ls -ln ~/permission-test'
sudo rm /srv/agentbuild/permission-test
```

`namei` 的输出会逐级显示路径中的每个组成部分。任何父目录都不应让无关组拥有写权限，因为组写权限可能让他人替换 `.ssh` 目录或操纵其中的文件。测试文件应显示该账户的数字 UID 和主 GID。

不要为了省时间，把你的 `.bashrc`、`.zshrc`、`.gitconfig` 或编辑器目录复制到这个主目录。这些文件经常包含私有包注册表、辅助别名、SSH 设置、凭据管理器和 Shell 钩子。等你能解释代理为什么需要某项设置后，再逐项添加。对于远程自动化，默认使用普通的非交互式 Shell 本来也更合适。

在代理的执行环境中设置保守的创建掩码。`umask` 设为 `077` 后，新建的普通文件默认只有该账户可读写，除非命令明确选择其他权限。这偶尔会暴露构建中的共享假设。这样很好。应有意修复构建的共享点，不要让每个生成文件都对所有本地用户可读。

## 共享代码库需要明确的所有权规则

代理应在自己拥有的代码库中工作，或者在一个你能解释其组和权限的项目目录中工作。开发者、部署工具和代理以互不相关的用户身份共同写入一个目录，第一次匆忙修复权限后就很难再理解。

最干净的做法是让代码库完全归代理账户所有。人类通过版本控制审查改动，或通过受控的组访问读取文件。这样可以消除大多数本地协作混乱，而 Git 提供了真正重要的交接机制。

有时代理必须写入公共构建目录。这时应创建专用项目组，并在共享目录上设置组 ID 位，让新文件继承该组：

```sh
sudo groupadd projectbuild
sudo usermod -aG projectbuild agentbuild
sudo install -d -m 2770 -o releasebot -g projectbuild /srv/project-build
sudo setfacl -m u:agentbuild:rwx /srv/project-build
sudo setfacl -d -m g:projectbuild:rwx /srv/project-build
```

这个例子需要根据你的所有权模型调整。重点不是访问控制列表是否流行，而是明确命名协作范围，并把写权限限制在这个范围内。不要遇到权限错误就运行 `chmod -R 777`，也不要把整个源代码树改成共享管理员组。两种做法都会把问题藏起来，直到有人写入不该写入的位置。

构建目录包含符号链接时，还会出现一个隐蔽问题。代理可能拥有 `/srv/project-build` 的写权限，但其中的符号链接可能指向 `/etc`、发布目录或某个人的主目录。在授予广泛的递归访问权限前，检查设置脚本和生成目录。账户边界只保护文件系统实际按照该账户权限评估的路径。

Git 也有相关的所有权检查。现代 Git 可能会拒绝一个看起来属于其他用户的代码库，并称其为「可疑所有权」。不要通过给代理的全局 `safe.directory` 设置加入任意目录来解决。应让运行 Git 的账户拥有该代码库。如果无法避免共享代码库，应记录其所有权，只有在理解 Git 为何拒绝后，才加入那一个明确的路径。

## SSH 访问应识别控制端并限制会话

为代理控制端使用专用 SSH 公钥，并在工作流允许时，将限制绑定到 `authorized_keys` 中的这把密钥。独立账户和不受限的交互式 Shell，已经比共享开发者登录账户好，但它仍然给了自主进程很大的命令操作面。

OpenSSH 在 `sshd_config(5)` 和 `authorized_keys` 文档中说明了可用控制项。`PasswordAuthentication no` 会在服务器层面禁用密码登录。`AllowUsers` 可以限制允许登录的用户。每把密钥的选项可以禁用端口转发、代理转发、X11 转发和伪终端分配。这些都是普通控制项，不是什么特殊 SSH 配置。

如果代理只需要接收 Git 命令或运行固定包装器，`authorized_keys` 中的一行可以这样写：

```text
restrict,command="/usr/local/libexec/agent-git-wrapper" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... agent-controller
```

`restrict` 是 OpenSSH 的简写选项，会关闭多种转发和会话功能，但具体取决于服务器版本和手册。强制命令意味着服务器会运行这个包装器，而不是接受客户端请求的命令。依赖某项限制前，先测试已安装的 OpenSSH 版本，因为旧主机可能不支持所有选项。

包装器必须自行验证输入。强制命令并不会把粗心的脚本变成安全边界。如果它把客户端提供的命令未经引号处理就传给 `sh -c`，客户端通常可以重新获得命令执行能力。包装器应保持简短，使用固定路径，拒绝意外参数，并记录请求。

如果通用编程代理需要 Shell 来检查、编辑、构建和测试，强制命令可能过于严格。保留专用密钥和账户，禁用不需要的转发，并在网络拓扑允许时限制来源地址。不要因为个人 SSH 密钥已经在笔记本电脑上，就让远程账户接受它。

检查 SSH 的生效配置，而不只是你编辑过的文件：

```sh
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin|allowusers|allowgroups'
ssh -vvv agentbuild@build-host true
```

第一个命令会打印服务器的生效设置。第二个命令会从客户端显示身份验证路径和被拒绝的方法。要使用代理实际的密钥进行测试。用自己的密钥测试几乎不能证明什么。

## Sudo 规则通常会抹掉刚刚建立的保护

不要给代理账户无限制的 sudo。`agentbuild ALL=(ALL) NOPASSWD: ALL` 会让独立 UID 基本只剩表面意义，因为代理运行的任何命令或脚本都能变成 root。

人们常在构建需要某个特权操作时添加这条规则，构建终于通过后却不再删除。狭窄的例外就这样变成了永久的主机控制权。如果任务确实需要提权，先问问是否应该由 root 所有的服务、部署工作节点或独立的管理员操作来完成。

`sudoers(5)` 手册提醒我们，命令匹配有边界，参数很重要，通配符可能匹配超出管理员预期的内容。允许编辑器、解释器、包管理器、代理可写的 Shell 脚本，或从可写目录加载配置的命令，都可能直接回到任意 root 执行。

更安全的模式是使用 root 所有且行为固定的包装器。假设代理必须在把已审查的产物放入固定目录后，重启一个指定服务。包装器应使用绝对命令路径，拒绝参数，验证输入的所有权和权限，并且只执行这次重启。随后 sudo 规则精确指定该包装器：

```text
Cmnd_Alias AGENT_RELEASE = /usr/local/sbin/restart-project-service
agentbuild ALL=(root) NOPASSWD: AGENT_RELEASE
```

使用 `visudo` 管理这条规则，并让包装器及其父目录归 root 所有，且 `agentbuild` 无法写入。这不能保证绝对安全，但能让一个狭窄的声明变得可测试：该账户只能执行一个 root 所有的程序，且没有调用者可控制的命令字符串。

如果你无法用一句话描述允许的特权操作，就先不要授予 sudo。继续拆分任务，直到能够做到这一点。这个不便是设计信号，不是粘贴宽泛规则的理由。

## 凭据需要与登录账户建立自己的边界

即使账户受到限制，只要主目录中有云凭据文件、范围过大的部署令牌，或能访问所有主机的 SSH 私钥，它仍然会很危险。不要把个人主目录中的凭据堆移到代理主目录，然后认为工作已经完成。

应为代理实际需要的操作、范围和环境签发凭据。能够向一个代码库推送的源代码管理凭据，与生产镜像仓库凭据不同。仅限一台主机的部署密钥，与在整个主机群中都被接受的私钥不同。通过账户名称、注释和撤销记录，让这些区别保持清晰。

避免在 Shell 环境变量中放置长期秘密。环境变量很容易通过诊断输出、子进程、崩溃报告和不同操作系统上的进程检查机制泄露。它们有时不可避免，但应当具有很短的生命周期，并通过严格控制的启动路径注入。

Sallyport 会在加密保险库中保存 HTTP 和 SSH 凭据，并执行请求的操作，不把凭据明文交给支持 MCP 的代理。当代理需要进行经过身份验证的调用，却不应在远程账户中拿到令牌文件或私钥时，这种方式很有用。

这里存在两个独立检查。远程 Unix 账户决定进程在那台机器上能做什么。操作网关决定代理进程能否请求带有 HTTP 或 SSH 操作权限的凭据。不要把它们合并成一个控制：凭据网关无法修复一个能读取生产文件的远程账户，而受限账户也无法阻止代理使用已经交给它的令牌。

对于 SSH 自动化，不要把个人私钥复制到 `/srv/agentbuild/.ssh`。创建专用凭据，并限制接受它的服务器。如果远程目标支持强制命令或来源限制，就使用它们。如果不支持，就在目标主机上限制账户。撤销应该意味着删除一份代理凭据，而不是更换日常管理使用的密钥。

## 日志必须区分意图与执行

记录代理会话、账户执行的命令，以及它实际做出的改动。只记录 `agentbuild logged in` 的日志，在你需要重建事件时没有帮助，因为它无法说明某项操作是人类请求的、代理提出的，还是远程脚本执行的。

Unix 已经提供了有用的证据。SSH 身份验证日志能识别接受的密钥和来源地址。根据主机情况，进程记账或审计设施可以追踪执行。版本控制记录提交和修改的文件，构建日志记录命令与产物。保持时间同步，以便比较这些记录。

不要只依赖 Shell 历史。非交互式命令可能不会进入历史，用户可以编辑历史，代理也可能运行会继续生成其他命令的工具。Shell 历史适合调试，但不是可信记录。

一次运行结束后，有效的审查应回答四个具体问题：

1. 哪个控制端以代理账户完成了身份验证？
2. 哪些命令或构建任务以该 UID 运行？
3. 预期工作区之外的哪些文件发生了变化？
4. 进程联系了哪些远程系统和已认证服务？

第三个问题可以尽早发现权限扩张。在事故发生前，将账户可写路径与预期工作区进行比较。进程列表也能暴露意外情况：如果一个本应短暂运行的构建代理留下了后台工作进程，那么编排会话结束后，这些进程仍保留该账户的权限。

Sallyport 会从加密、哈希链式审计日志中记录代理会话和单独操作，`sp audit verify` 可以在离线状态下验证链条，无需保险库密钥。只有同时保留远程主机上的证据，证明获准的 SSH 操作到达后实际做了什么，这份记录才最有价值。

## 共享开发者登录账户会以可预见的方式失败

失败通常始于一个合理请求：让代理运行和你相同的测试。开发者把代理指向现有远程主机，并授权自己的普通 SSH 密钥，因为代码库、包缓存和构建依赖已经配置完成。

代理运行测试命令。测试初始化程序读取开发者环境，发现一个代码仓库令牌。安装依赖时执行了 postinstall 脚本。这个脚本可以读取开发者的主目录，查看 SSH 配置，使用任何可访问的凭据助手，并通过开发者已有的网络范围进行连接。根本不需要利用内核漏洞。进程只是拥有与开发者相同的权限。

之后，部署脚本因为需要一个可写产物路径而失败。有人用宽泛的组权限修改修复它。现在该账户可以写入发布目录。第二次修复因为服务重启被阻止，又加入了免密码 sudo。到这一步，所谓的代理配置已经继承了开发者身份、广泛的文件系统写权限、可复用凭据和 root 提权能力。

独立账户会改变这条失败路径。第一次测试可能因为账户无法读取代码仓库配置或写入旧的共享缓存而失败。这个失败很有用。它告诉你应该签发范围受限的代码仓库凭据、创建账户拥有的缓存目录，或重新设计构建过程。每次修复都会变成可以审查的明确授权。

要预期早期摩擦。如果新账户第一次针对成熟的开发者环境运行就完全正常，应仔细检查。这可能意味着主机已经向所有本地用户开放了过多数据和权限。

## 以代理身份测试边界，再删除意外权限

在允许无人值守工作前，从独立的管理员会话测试这个账户。不要只切换 Shell 提示符，就假定身份已经干净地改变。使用专用密钥进行身份验证，执行实际的启动命令，并从会话外部观察结果。

每次发生重要访问变更后，运行这项简短的验收检查：

```sh
ssh -i ./agentbuild_key agentbuild@build-host 'id; umask; pwd; find ~ -maxdepth 1 -printf "%M %u %g %p\\n"'
ssh -i ./agentbuild_key agentbuild@build-host 'sudo -n true; echo sudo_status=$?'
ssh -i ./agentbuild_key agentbuild@build-host 'find /srv/project-build -xdev -type f -perm -0002 -print'
```

第一行确认身份、工作目录、创建掩码和主目录权限。第二行通常应返回非零状态，因为账户不应拥有通用 sudo 权限。第三行搜索共享项目目录中的全局可写普通文件。如果系统的 `find` 不支持 GNU `-printf`，可以改用 `ls -ld` 和 `stat`。

然后刻意测试被拒绝的操作。尝试读取某个人的主目录、写入工作区之外的位置、使用个人部署密钥路径，以及在应该禁用转发时建立 SSH 转发会话。没有实际测试过的限制，只是配置文件中的意图。

真实任务运行后也要审查账户。不再需要时，删除过期的授权密钥、组成员身份、缓存、临时 ACL 和部署权限。权限清理很少会在截止日期前自动发生。把它列入任务完成条件。

先创建一个只能访问自身主目录和测试代码库的账户。只有当真实命令失败，并且你能准确说明所需权限时，才增加一项能力。设置阶段看起来会慢一些，但远比事后梳理自主进程从开发者登录账户中复制、使用或破坏了哪些内容要快。
