解决AI Agent长连接场景下密钥轮换导致飞行中消息失败的三种失败模式和应对策略。
你的 agent 已经和另一个 peer 聊了六个小时。有正在飞行的请求、有签名密钥、有 API token、还有通过 NAT 发现的长期隧道。安全部门说:周五之前把所有凭证都轮换一遍。你换掉密钥——结果所有飞行中的消息全挂了、所有重连都失败了,对端缓存的身份和你现在呈现的完全对不上。AI agent 凭证轮换而不影响服务,这个问题是没人给你写操作手册的,所以这是我自己踩坑之后整理出来的版本。
Web 服务随时都在轮换密钥。区别在于,无状态的 HTTP 请求生命周期只有毫秒级:等密钥轮换的时候,所有飞行中的请求早就完成了。Agent 是有状态的。它们会保持会话开放几小时甚至几天,轮换那一刻正在飞行中的消息,是用旧密钥签发的。
实际中会出现三种失败模式:
飞行中的请求被拒绝。 用旧密钥签名的消息在轮换之后才到达。严格校验会直接丢弃它。对端重试,重试用新密钥签名,于是两端对哪个密钥有效这件事产生了分歧。
Peers 缓存了你的身份。 你的 peer 在握手时就记住了你的公钥(或 token)。硬切换会让缓存的身份失效,强制在对话中途重新握手。
重连风暴。 旧凭证一失效,所有本来静默连接的 peer 同时尝试重建连接。这是做密钥交换最糟糕的时机。
这些都不新鲜——TLS 和密钥基础设施几十年前就解决了大部分问题。诀窍是在需要之前把这些方案迁移到 agent 场景。
零停机轮换的核心思想很简单:不要出现旧密钥已失效而新密钥还未被信任的那个时刻。在重叠窗口期内让两者同时存活。
版本化密钥。 给每个密钥一个 kid(key ID)加上激活窗口。用最新的活跃密钥签名;用窗口尚未关闭的任何密钥验签。这和 JWKS 轮换的工作方式完全一致,也是 TLS 交叉签名证书平滑 CA 迁移的做法。
class RotatingKeyring:
"""Old keys stay verifiable during the grace window; only signing moves forward."""
def __init__(self, keys): # [{"kid", "key", "active_from", "expires_at"}]
self.keys = keys
def sign(self, payload):
current = max(self.keys, key=lambda k: k["active_from"])
return current["kid"], sign(current["key"], payload)
def verify(self, kid, sig, payload):
entry = next(k for k in self.keys if k["kid"] == kid)
if entry["expires_at"] < now():
raise KeyExpired(f"{kid} outside grace window")
return verify(entry["key"], sig, payload)
宽限期大小。 重叠窗口必须覆盖最坏情况:消息最长的飞行时间,加上 peer 最长可能注意到新密钥的时间,再加上重连退避。以 keepalive 间隔的几倍作为合理起点是靠谱的。
交错轮换。 一次只轮换一个节点,观察错误率,然后再继续。全fleet统一轮换会把一次失误变成千人在场的故障。
短期密钥。 如果 token 每小时过期而不是每年过期,轮换就不再是消防演习,而是一个后台事件。来自密钥管理器(Vault 风格的租约是经典例子)的动态密钥免费给你这个能力:agent 自己获取、使用、刷新,不需要人工介入。
标准方案能覆盖大部分场景。Agent 引入了三个新麻烦:
飞行中的消息是签名过的,不只是授权过的。 Token 校验发生在请求时;签名校验发生在验签时,可能在发送方已经轮换之后。你的验签路径必须容忍旧密钥一段时间。
身份本身就是凭证。 如果你的 agent 的"密钥"同时也是它的身份——peer 用来识别它的东西——轮换它和一个新节点出现是无法区分的。Peers 会把轮换后的 agent 当陌生人。
传输层是有状态的。 Agent 通常位于 NAT 后面,带有映射端点。守护进程重启来"应用新密钥"可能会丢失那个映射,agent 在下一次 keepalive 之前都不可达。需要重启的轮换就是停机,只是被延迟了。
贯穿始终的主线是:将身份和凭证解耦。你是谁应该是稳定的;你呈现出来做认证的东西应该是可替换的。
TLS 用长期证书和临时会话密钥做到了这一点:证书是身份,每次连接的密钥交换是一次性的。Agent 基础设施需要同样的分离,而这正是 Pilot Protocol 的设计选择。
Pilot Protocol 是一个面向 agent 的开源覆盖网络——每个 agent 一个永久虚拟地址,加密 UDP 隧道,NAT 穿越,以及逐 peer 的信任模型。有两个特性让轮换变得平淡无奇:
寻址与密钥解耦。 每个 agent 在注册时获得一个稳定的虚拟地址(如 0:0000.0000.0001)。它穿越重启、IP 变更和跨云迁移而存活。Peers 通过地址找到你,而不是通过密钥——所以轮换密钥材料不会改变你被找到的方式。
隧道密钥是按连接生成的。 每条隧道通过 X25519 密钥交换派生自己的共享密钥,流量用 AES-256-GCM 加密。长期身份材料(~/.pilot/identity.json 中的 Ed25519 密钥对)用于信任握手签名,而不是数据通道。轮换长期材料不会拆除活跃隧道,因为活跃隧道不是用它加密的。
这种分离是使无停机轮换在结构上成为可能而不是一串幸运时序的关键。工具链把它当作一等公民的操作:pilotctl rotate-key 生成一个新的身份密钥对,而且有文档化的恢复流程(pilotctl recovery enroll / new-key / recover),所以即使密钥丢失也不会丢失地址——你轮换到新密钥,然后用恢复材料找回同一个地址。
信任的处理方式也是如此:明确的逐 peer 握手,跨越守护进程重启持久化,可以用 untrust 撤销。成员资格和信任是分开的——轮换密钥不会重置你的信任图,撤销一个 peer 不需要重新给网络设置密钥。Pilot Protocol 文档详细介绍了寻址、传输和信任模型。
这不是魔法——这就是让 TLS 轮换变得平淡无奇的同一套身份/凭证分离,只是应用到了 agent 的整个网络层而不是单个连接。
无论你使用什么传输层,同样的检查清单适用:
"无停机凭证轮换"对 agent 来说意味着什么? 在 agent 继续运行、继续与 peers 通信的同时更换 API token、签名密钥或身份密钥对——不丢失飞行中的消息、不强制重连、不出现 agent 不可达的窗口期。
重叠窗口应该多长? 长到足以覆盖最长的飞行中消息加上 peer 注意到新密钥所需的时间,再加上重连退避。以 keepalive 间隔的几倍作为合理起点。
轮换密钥需要重启 agent 吗? 只有在你的架构把凭证耦合到进程的情况下才需要。如果身份和凭证解耦了,隧道携带的是按连接生成的会话密钥,轮换就是一个配置级别的操作。
轮换 API token 和轮换身份密钥有什么区别? Token 授权请求,轮换成本低。身份密钥是 peers 用来识别你的东西;在没有重叠窗口的情况下轮换它,看起来就像一个新节点出现。这就是稳定地址模式重要的原因。
轮换是正常的维护事件,不是等待发生的故障。能存活下来的 agent,是那些密钥变了但身份不变的 agent。
curl -fsSL https://pilotprotocol.network/install.sh | sh
然后 pilotctl rotate-key 和 pilotctl recovery enroll 只需一条命令。文档:pilotprotocol.network/docs