# AI 编程代理的 DNS 变更：让审核更安全

AI 编程代理执行 DNS 变更时，需要比普通基础设施编辑更严格的操作形式。代理绝不能把「更新 DNS」作为一个审批项。它应将创建、编辑和删除分别列为不同操作，因为它们需要不同的证据、不同的审核问题和不同的失败处理方式。

只有当代理替换邮件记录、在不兼容的数据旁边添加 CNAME，或在迁移期间删除最后一条地址记录时，这种区分才不会显得繁琐。DNS 往往让细小的文本改动看起来无害，但影响会扩散到解析器、客户端、委派区域、证书检查、邮件投递和服务发现。审核者必须先看到准确的操作动词，才能判断影响范围。

## DNS 记录动词代表不同类型的风险

创建 DNS 记录，会向区域中加入一条新的声明。审核者需要确认名称由谁负责、目标是否正确、新数据是否与现有数据冲突，以及是否有人可能利用该记录完成域名验证或劫持流量。

编辑记录，会改变已有的声明。这个操作需要并排显示旧值和新值。将一个地址改成另一个地址，可能会转移应用流量。修改 TXT 记录，可能会改变发件人认证、所有权验证，或外部服务读取的配置。旧状态能帮助审核者区分有意替换和代理基于过时信息采取的行动。

删除记录，意味着现在正确的状态是不存在这条记录。它的失败范围最广，因为安全结果往往取决于 DNS 之外的因素。也许新端点需要一段时间才能接收流量。也许另一个团队仍在使用验证 TXT 记录。也许这条记录属于代理无法从代码库推断出的故障转移配置。

把这些操作当成一种通用写入，会让审核者失去警觉。当每张卡片都写着同样的内容时，审核者会一路点击通过，直到太晚才发现其中一张卡片删除了整个 RRset。控制机制必须让这种危险差异无法被忽略。

DNS 术语也需要谨慎处理。DNS 所有者名称可以对应一个 RRset，也就是名称和类型相同的一组记录。许多服务商 API 暴露的是单个「记录」对象，但更新行为可能会替换整个 RRset。如果代理想删除两个 A 值中的一个，而服务商却替换整个集合，就可能意外删除两个值。在确定某个操作的含义前，要先了解服务商实际使用的对象模型。

## 让每个请求声明一个不可逆的操作

DNS 操作应只包含一个动词：`create`、`edit` 或 `delete`。不要在审批边界接受名为 `apply`、`sync` 或 `upsert` 的模糊方法。这些名称对程序员很方便，却会增加运维人员的判断成本。

请求还必须说明它操作的是一个服务商记录对象，还是整个 RRset。如果 API 无法安全地编辑单个成员，操作就应明确说明它会替换整个 RRset，并列出所有保留的值。对这一点保持沉默，会让一次看似很小的地址修改变成中断。

可以采用下面这种请求结构，既保留意图，也为执行器提供足够的状态来拒绝漂移：

```json
{
  "action": "edit",
  "zone": "example.net",
  "owner": "api.example.net.",
  "type": "A",
  "expected_rrset": {
    "ttl": 300,
    "values": ["198.51.100.24"]
  },
  "proposed_rrset": {
    "ttl": 300,
    "values": ["198.51.100.81"]
  },
  "reason": "Move the API endpoint after the service owner confirmed health checks",
  "verification": [
    "authoritative lookup returns 198.51.100.81",
    "HTTPS health check succeeds at the new endpoint"
  ]
}
```

对于创建操作，`expected_rrset` 应说明 RRset 不存在，或者在服务商模型有要求时，写出允许的现有状态。对于删除操作，`proposed_rrset` 应明确写成不存在。不要用空字符串表示缺失，也不要省略字段。含义不清的载荷会产生含义不清的日志。

执行器应在写入前立即读取当前区域状态，将观测结果与 `expected_rrset` 比较，任何不一致都应停止。这是一种比较并交换的纪律，即使 DNS 服务商没有提供比较并交换端点，也应如此执行。

RFC 2136《域名系统中的动态更新》直接在协议中体现了这一思想。它的前置条件部分允许客户端声明某些 RRset 在服务器接受更新前必须存在或必须不存在。多数托管 DNS API 不会暴露 RFC 2136 的线路消息，但操作层面的结论仍然成立：基于未经验证的旧读取结果进行写入并不安全。

代理可以重试读取操作，但不应在写入失败后把预期状态改成它当前看到的状态再重试。状态不一致意味着其他操作者修改了 DNS，代理的计划已经过时，或者服务商以规划器没有预料到的方式规范化了数据。应将这种情况交给人工审核者处理。

## 创建操作需要所有权和冲突证据

新记录在获批前，需要证明该名称属于预期的服务。代码库路径、工单引用或自然语言请求都可以提示所有权，但它们无法证明新的公网名称确实可以使用。

代理提出创建建议前，应检查准确的所有者名称在各种记录类型下的情况。CNAME 尤其需要注意。RFC 1034 规定，如果某个节点存在 CNAME RR，则该节点不应有其他数据。只检查现有 CNAME 的规划器，可能会在已经存在地址、邮件或 TXT 数据的名称上提出 CNAME。服务商可能拒绝写入，也可能按照特定规则替换该名称下的数据。

创建审核应以清晰的语言展示四项内容：

- 完整的所有者名称、区域、记录类型、值和 TTL。
- 同一所有者名称下的当前记录，包括代理不会修改的类型。
- 谁请求了该名称，以及哪个服务会使用它。
- 适用于该记录类型的目标检查。

目标检查取决于数据类型。对于 A 或 AAAA 记录，代理可以在存在部署清单时确认地址属于预期的部署清单。对于 CNAME，应解析目标并确认目标名称完整。对于 MX，应与邮件负责人确认邮件主机名和优先级。对于供外部验证器使用的 TXT，应保留准确的原始值，不要凭猜测清理空格或调整引号。

不要让代理因为新请求的子域位于熟悉的域名之下，就推断它无害。`login`、`auth`、`mta`、`vpn` 和 `admin` 的后果显而易见，但任意名称也可能很重要。公网 DNS 名称一旦被依赖，就会成为一个接口。

创建操作与发布部署也不同，因为回滚不一定意味着删除。如果新记录支持验证流程，流程完成后删除它可能会破坏续期或未来的验证。提案应说明记录是否临时存在、由谁决定何时过期，以及请求者是否接受日后删除。如果没有人负责回答这些问题，就不要擅自设置过期日期。

## 编辑操作必须说明要替换的状态

只有当提案明确标出将要替换的准确状态时，编辑才是安全的。「让应用指向新主机」只是意图，不是可执行的 DNS 操作。

每个编辑请求都应包含旧 RRset，包括代理计划保留的值。假设一个 A RRset 有两个地址：

```text
Current:  app.example.net. 60 IN A 198.51.100.10
          app.example.net. 60 IN A 198.51.100.11
Proposed: app.example.net. 60 IN A 198.51.100.11
          app.example.net. 60 IN A 198.51.100.12
```

这是一次受控轮换。审核者可以看到操作保留了一个服务地址，同时替换了另一个。如果服务商按整体替换 RRset，那么只发送 `198.51.100.12` 就是把删除和创建伪装成编辑。

TTL 变更也属于同一张审核卡片。团队经常在迁移前降低 TTL，迁移后再提高 TTL。这两种变更都有后果。较低的 TTL 会增加解析器查询频率，也可能更快暴露配置错误。较高的 TTL 可能让流量在错误编辑后更长时间停留在错误端点。任何一种变更都不应藏在普通记录更新中。

代理还应区分数据编辑和格式差异。服务商可能会规范化所有者名称、添加末尾的点、拆分 TXT 字符串或重新排列值。规划器如果直接比较 API 返回的原始文本，就会产生多余的审批卡片或虚假的冲突。比较前应先规范表示形式，但要保留审核者需要读取的语义数据。

不要因为代理在版本控制中找到了匹配字符串，就直接批准编辑。版本控制中的内容可能是期望状态，但紧急 DNS 变更后，它可能已经失去相关性。当前权威状态仍然是写入条件。代码库可以解释意图，DNS 才能告诉你客户端实际会收到什么。

## 删除操作需要说明为什么现在可以不存在

删除请求应回答为什么记录现在可以消失，而不只是说明代理为什么觉得它不重要。旧记录看似多余，直到有人发现它仍然支持旧版回调、邮件系统、证书验证器或委派服务。

安全的做法是将流量迁移与清理分开。先添加或编辑替代数据，然后验证预期的权威响应和依赖服务。只有服务负责人确认新行为后，代理才应请求删除旧数据。即使服务商可以将两者合并，也要让删除请求保持独立。

删除请求应包含以下事实：

- 要删除的准确 RRset 或服务商记录对象。
- 确认不再依赖该数据的服务或团队。
- 替代操作及其验证结果，如果存在替代操作。
- 删除后的计划验证。
- 删除是否会改变整个 RRset。

区域顶级记录需要格外谨慎。不同服务商对区域顶级的处理方式不同，尤其是提供模拟 CNAME 行为的别名或合成记录时。代理绝不能在没有熟悉该服务商语义的审核者参与下，把期望的 CNAME 转换成服务商专用的顶级功能。DNS 中熟悉的词语，并不保证行为也熟悉。

删除后恢复会受到否定缓存影响。RFC 2308 说明了解析器如何缓存否定答案，包括 NXDOMAIN 和无数据响应。删除记录后再恢复时，一些客户端仍可能在否定缓存过期前继续看到之前的缺失状态。这不表示禁止删除，而是说明回滚计划必须包含这样一段时间：权威 DNS 已经修复，但用户仍然收到缓存的失败结果。

如果服务商拒绝删除单个值，删除操作不应默默扩大为其他操作。执行器应报告服务商要求替换整个 RRset，并请求新的明确操作。多一次审批，比解释为什么一条所谓未使用的地址记录消失了要便宜得多。

## 审核卡片需要超出区域差异的上下文

原始差异能提供数据，却经常隐藏后果。审核卡片应先用一句简短的话说明操作和预期行为，例如：「删除 `verify.example.net` 的过时 TXT 验证记录；请求者已确认外部验证完成。」然后再显示准确记录。

无需打开其他视图，卡片就应展示以下字段：操作动词、区域、所有者名称、类型、旧状态、建议状态、TTL、请求者、代理进程身份、原因和验证计划。如果写入可能影响整个 RRset，应在第一行说明。

使用直接的措辞。「替换两个 A 值」比「协调记录集」更好。「移除这条 MX 记录」比「应用期望配置」更好。使用操作动词而不是抽象表达，审核者会更快作出判断。

有用的卡片还应告诉审核者什么情况足以拒绝操作。对于 CNAME 创建，要显示该所有者名称下的冲突数据。对于编辑，要显示当前状态是否不同于预期状态。对于删除，要显示代理是否缺少服务负责人的确认。审批界面应公开不确定性，而不是把它藏在执行日志中。

不要通过提供大段自由文本的例外框，把审核者变成手动策略引擎。如果代理请求的操作不符合常规结构，就要求它补充缺失证据并准备一项新的操作。让审核者从聊天记录中重新推断 DNS 语义，迟早会导致错误获批。

### 一个完整的审核示例

假设代理需要将 `api.example.net` 移到替代地址。一张糟糕的卡片只写着：「更新 API 的 DNS。」审核者无法看出服务商会替换一个包含两个成员的 RRset。

一项可审核的编辑操作应写成：「将 `api.example.net` 的 A RRset 中的 `198.51.100.24` 替换为 `198.51.100.81`；保留 `198.51.100.25`；TTL 保持为 300。」随后展示完整的当前和建议 RRset，写出服务负责人，并说明代理将在写入后查询权威名称服务器并运行获批的健康检查。

如果审核者希望执行没有重叠期的切换，就应拒绝这张卡片并明确提出该意图。代理不应因为部署文件中出现了新地址，就自行判断不需要重叠。

## DNS 缓存让时机成为审批的一部分

权威 DNS 和递归 DNS 回答的是不同的运维问题。权威服务器告诉你区域当前发布的数据。递归解析器可以在收到的 TTL 过期前返回旧答案。浏览器、操作系统、应用运行时、负载均衡器或内部解析器还可能增加另一层缓存。

因此，审批应明确期望的验证点。「权威服务器返回新答案」验证的是写入。「公共解析器返回新答案」验证的是部分传播。「应用在新目标上提供流量」验证的是用户真正需要的结果。这些是不同的检查。

使用能够显示答案来源的命令。将名称和地址替换为区域实际使用的权威服务器：

```sh
dig @ns1.example.net api.example.net A +noall +answer

; expected answer shape
api.example.net. 300 IN A 198.51.100.81

dig @1.1.1.1 api.example.net A +noall +answer

; resolver answer can retain an older value with a lower remaining TTL
api.example.net. 117 IN A 198.51.100.24
```

第一条查询检查一台权威服务器上的已发布状态。第二条查询检查一台递归解析器当前返回的内容。两者都不能证明所有客户端都已经切换。应将两项结果都记录在操作日志中，让事故响应人员能够区分服务商写入失败和预期的缓存行为。

不要把 TTL 当成全球一致性的倒计时。解析器缓存答案时会接收一个 TTL，因此不同解析器会在不同时间开始倒计时。一些客户端还可能独立缓存连接或应用配置。迁移计划需要服务级验证，而不是承诺等待若干秒后就一定完成。

DNS 委派变更需要更高程度的谨慎。编辑 NS 记录、胶水记录、DS 记录或其他与委派有关的数据，可能影响子区域之外的解析。代理应将这些操作归类为单独的变更类型，并要求同时负责父区域与子区域关系的审核者参与。不要把它们纳入普通主机记录流程。

## 宽泛的 DNS 凭据不是捷径

给代理一个可以编辑所有区域的服务商凭据，看起来很高效，因为它无需等待集成变更就能完成任务。但这不是正确的权限边界。提示词注入、错误的代码库指令或混乱的计划，都可能因此获得远超目标服务所需的权限。

将每条执行路径限制在所需的区域和操作范围内。如果服务商支持，应将读取权限与写入权限分开。建立代码库或服务身份与可请求区域之间的映射。在请求到达服务商凭据之前，就拒绝超出映射范围的区域请求。

权限边界应限制操作本身，而不是只把令牌藏起来。如果代理可以要求中间层发送任意服务商 API 请求，而中间层接受任意路径、方法、请求头和请求体，那么代理仍然拥有广泛控制权。操作层应负责 DNS 服务商调用的结构，并拒绝架构之外的字段。

这正是通用 `upsert` 端点带来问题的地方。它让代理可以用一个请求体把创建、编辑和删除合并起来，而且常常还会使用审核者看不到的服务商默认值。应定义独立的执行器方法，让每个方法验证操作所需的证据。删除方法应拒绝包含替代状态的载荷。编辑方法应拒绝缺少预期状态的请求。

对于使用 Sallyport 的团队，DNS 凭据应留在代理进程之外。应用可以执行 HTTP 调用，同时将凭据保存在加密保险库中，让代理只接收结果而不是凭据。这能解决凭据暴露问题，但不能判断 DNS 操作建议是否合理，判断工作仍由操作结构和审批证据完成。

## 过时的计划可能删除错误的线上记录

一种常见故障始于代理在一次漫长的编程任务开始时读取 DNS。它看到 `cdn.example.net` 只有一个 CNAME 目标，于是计划在部署后替换该目标。代理工作期间，值班工程师为了事故处理而更新目标，临时转移了流量。

代理稍后完成并发送旧的替换请求。如果服务商调用使用通用更新，或使用无条件的先删除后创建流程，就会覆盖值班工程师的变更。相对于代理数小时前读取的状态，这个请求可能看起来正确；相对于线上区域，它却是错误的。

写入前重新读取可以防止这类故障。执行器读取 RRset，将其与操作中的 `expected_rrset` 比较，如果值班工程师设置的目标不同，就拒绝写入。拒绝记录应保留两个状态，并告诉审核者记录已被其他操作者修改。不要尝试合并。

合并 DNS 数据需要服务知识。两个地址可能代表有意的容量配置、临时重叠、区域拆分或紧急路由。代理不能仅凭部署文件中只出现一个地址，就安全地推断另一个地址应该被删除。

部分失败后也适用同一规则。如果服务商报告超时，执行器必须在重试前读取服务商的权威状态。写入可能已经成功，只是客户端没有收到响应。盲目重试会在某些 API 上创建重复值，在另一些 API 上产生不需要的替换行为。

## 日志必须保留审核者真正批准的内容

DNS 审计记录不能只有一条说明记录发生变化的服务商活动事件。应保存建议操作、执行前观测到的状态、审批决定、发起请求的进程、服务商响应和验证结果。保留足够的信息，才能还原执行器是否遵循了获批请求。

防篡改日志还能提供一个有价值的特性：调查事故的人可以检查历史是否在事后被修改。当 DNS 答案已经从缓存中过期，人们开始依赖记忆或截图时，这一点尤其重要。即使没有原始代理对话，证据也应保持可理解。

将运行记录与调用记录分开。运行记录回答哪个代理进程在何时获得权限，以及权限何时结束。调用记录回答它请求了哪一项准确的 DNS 操作、是否有人批准，以及结果如何。立即撤销一次运行可以阻止之后的调用，但不能撤销服务商已经接受的 DNS 写入。审计轨迹应清楚记录这条边界。

在审批卡片和执行记录中都使用操作 ID。如果操作者看到错误结果，应能通过操作 ID 找到准确获批的删除或编辑，而不必在无法区分的代理输出流中搜索。该 ID 应连接请求、状态比较、服务商响应和验证命令。

第一项实际改动很小：在审批边界禁止通用 DNS 写入。强制每个请求归入创建、编辑或删除之一，要求编辑和删除提供完整的预期 RRset，并在执行前拒绝状态漂移。仅这一项约束，就能让代理提出的 DNS 工作足够清晰，使人可以发现真正重要的错误。
