定时任务和站在登录提示符前的人以不同的凭据、不同的生命周期和不同的所有者调用同一个 API。任务需要网络路径、应用身份和一组权限;人则需要登录、第二因素和会话。把两者混在一起,就会产生一种在人的密码每次变更时都会中断的自动化,或者一种为了维持 cron 任务存活而被削弱到刚好够用的多因素策略。

三个看起来像一个的关卡

源地址规则问的是请求从哪里来。身份验证问的是谁在调用、出示了哪些证明。授权问的是这个主体能否执行这项操作。VPN 只改变三者中的第一个,而且改变的是主机上每个进程而不只是任务,所以在事件记录里要把这三个观察分开。

这就是为什么更换出口节点可以修好源地址造成的拒绝,却让无效令牌原封不动。这也是为什么有效的 API 令牌并不能让一个没有路由的任务到达私有端点。

交互式路径是糟糕的自动化模板

把人的登录写成脚本很诱人,因为它能奏效一次。脚本会存下密码,也许还有一次性密码种子,然后依赖一个被设计成必须由人来完成的挑战。OAuth 当前的最佳安全实践 RFC 9700 禁止 resource-owner-password-credentials 授权,并解释了它为什么不适配双因素和多步验证。

受支持的替代方案通常是机密客户端流程:授权服务器验证应用并签发令牌。RFC 6749 第 4.4 节的 client-credentials 授权就是一个例子。它的存在并不意味着每个 API 都接受它,也不意味着可以用它替换用户委派的任务,因为那些是服务所有者要回答的问题。写代码之前,先问清服务支持哪种无人值守模型。

VPN 仍然属于哪里

VPN 可以提供私有 API 所需的网络路径,这是正当角色。它并不向应用验证任务,也不界定任务能做什么。共享出口地址识别的是主机而不是任务,所以为该地址加一条允许列表条目,会放进主机上运行的每一个进程。

如果任务运行在共享 runner 上,这个区别更重要而不是更不重要。同一主机上的另一个工作流会继承路由和地址,API 无法区分两者。这和共享密码是同一个弱点,只是下移了一层。把限制放在应用能够强制执行的地方;如果无法把允许列表收得更紧,就隔离 runner。

诚实地比较两种设计

人工登录给你一个真实的人、真实的第二因素和可审计的会话,但它无法无人值守地运行。应用身份给你无人值守的执行、可审阅的范围和可轮换的凭据,但它无法代表一个没有委派任何东西的用户行动。两者都不是对方的弱化版本,它们回答的是不同的问题。选择的方法是问:任务是否必须在无人到场时运行,操作是否必须归属于某个特定用户。

如果两个答案都是肯定的,设计需要的是委派模型而不是借来的密码。把这个决定写在下一个维护者能找到的地方。

交接前要验证什么

从真实 worker 运行一次被允许的操作和一次必须的拒绝,并保留两个结果。然后测试过期凭据和网络路径不可用时的行为,因为正是这两种故障会在夜里把人叫醒。

curl -sS -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer TOKEN" https://api.example.com/v1/records

把主体、范围、轮换所有者和执行环境记录在那个结果旁边。正是这份记录让下一次轮换成为例行工作而不是事故。它也是审计人员要的那部分。

相关文章