Authorization 是现代安全体系的核心机制。无论采用 RBAC、ABAC、PBAC 还是更动态的上下文感知授权,它回答的都是同一类问题:当前主体,在当前条件下,是否被允许对某个资源执行某种操作。这个问题被研究得非常透彻,工程实现也相当成熟——系统可以综合身份、角色、属性、设备状态、风险评分、时间窗口、资源敏感度乃至完整的事务上下文做出判断。对绝大多数业务系统而言,只要授权判断足够准确,安全控制就已经达到了很高水平。

之所以"足够准确"在过去基本等于"足够安全",是因为在以读取和记录为主的系统里,判断与结果之间几乎没有距离。授权通过,数据就被读到了;授权拒绝,什么都不会发生。判断本身几乎就是结果。

而当机器主体开始直接改变资金、生产环境、基础设施和物理设备状态之后,判断与结果之间开始出现一段真实的、有长度的、可以被干预的过程。授权在这段过程的起点做出,现实变化在这段过程的终点发生。中间这一段谁在管,过去很少需要单独回答,现在需要。

一、Authorization 输出的是一个判断,Execution Control 管理的是让判断落地的能力

授权的典型输入可以抽象为:

Subject + Action + Resource + Context → ALLOW / DENY

系统检查身份、环境、角色、时间窗口、风险评分等条件,返回 ALLOW。到这里,授权已经完成了它被设计来完成的全部任务:当前主体对当前动作与资源具备授权资格。

但如果这个动作会进一步触发真实执行,系统还要走完一段从判断到现实的路径:

Authorization Decision → 参数构造 → 凭据使用 → 执行请求 → 外部系统 → 状态改变

真正改变现实的,不是 ALLOW 这个判断,而是这条路径后半段的一系列行为。判断是一个命题,它本身没有因果力;改变现实的是某种能力——云平台凭据、支付接口凭据、签名密钥、工业控制指令、设备执行接口、生产部署权限。

这就引出一个需要被单独命名的分层:做出判断的权力(Decision Authority)与让动作发生的权力(Execution Authority)不是同一个东西。策略引擎完全可以拥有前者——它有能力认定某个请求满足规则;但它是否理所当然地应该同时持有最终凭据、并直接让动作发生,这不是一个可以从"它负责判断"推出来的结论。这两种权力在多数系统里被合并在一起,往往不是因为架构上论证过应该合并,而是因为它们恰好写在同一个服务里。这一点在下一篇会被专门展开,本篇只需要确认它们在概念上可分。

Authorization 证明某项操作可以被允许;Execution Control 保证最终发生的仍然是被允许的那一项操作。

二、授权判定的是一个请求,执行面对的是一组参数:语义必须被完整携带

再次强调,本篇的论点不建立在"授权只能判断角色"这种过时假设上。一条策略完全可以写成"Agent-A 可以向供应商组 X 执行支付,单笔不超过一万,仅限工作时间,且当前风险评分低于阈值"。这已经非常接近一个执行边界的描述。

问题出在这份判定结果如何被传递。授权阶段判定的是 Target = Supplier-A, Amount = 5,000, Risk = Normal 这一组具体取值,于是返回 ALLOW。而如果 Agent 或链路上的某个组件随后重新构造了执行参数,把它变成 Target = Supplier-B, Amount = 50,000,执行侧收到的却只是一个 Authorization = ALLOW,那么这个 ALLOW 已经与它最初判定的那件事脱钩了。

上一篇讨论的是各项条件之间是否真的相互绑定;本篇要问的是另一件事——一份已经正确成立的判定,能否原样抵达真正改变现实的位置。这是一个表示与传输的问题。

由此可以得到一个容易被当成 API 设计细节、实则是安全属性的判断:决策结果的表示形式决定了它能携带多少约束。一个布尔型的 ALLOW 在离开策略引擎的那一刻就丢失了全部上下文,下游无从区分"批准了这一次具体执行"和"批准了这一类调用"。要让语义完整传递,决策结果必须是对被判定的那个具体执行对象的可验证表述:

ALLOW(Action-A, Target-B, Parameter-P) 不能被解释成 ALLOW(any call to this API)

这两者在类型系统层面是不同的东西,但在很多实现里被压缩成了同一个布尔值。压缩发生的位置,通常就是执行语义开始流失的位置。

细粒度 Authorization 决定什么可以被批准;Execution Control 保证被批准的对象不会在抵达执行点之前被替换。

三、Policy Enforcement Point 不一定是最终执行点

传统授权架构会强调策略执行点:某个位置负责落实 ALLOW 与 DENY,例如 API 网关在收到拒绝时挡下请求。这是一个成熟且有效的模式。

需要追问的是这个位置在整条执行路径上处于哪里:

Agent → API 网关 → 内部服务 → 消息队列 → Worker → 支付服务商

网关可以把授权做得完全正确,但它与真正不可逆的那一跳之间还隔着若干组件。如果其中任何一个内部服务或 Worker 能够修改金额、替换对象、重新生成请求,或者本身持有最终支付凭据,那么真正的执行 Authority 位于网关之后。

由此可以给出一条比"纵深防御"更精确的表述:执行路径上的安全强度,不由链路中最强的那道约束决定,而由最靠近不可逆点的那道约束决定。判定点与执行点之间每多一跳,就多一个能够重构参数的主体;这些主体是否可信、是否在威胁模型内,直接决定了前端那次正确授权还剩下多少效力。

异步化让这段距离进一步拉长,也带来一个原本不存在的问题。队列、重试与批处理会系统性地把决策与执行分开,而重试机制意味着同一个决策结果可能被使用多次。如果 Worker 因为超时重试三次,这究竟应该是一次执行还是三次执行,取决于那份 ALLOW 是否被设计成一次性的、是否带有可校验的唯一执行标识、以及执行侧是否具备判重能力。在资金和物理动作场景中,这个问题的答案不是性能选项,而是安全属性。一个可以被重放的授权结果,等价于一张没有面额上限的支票。

Authorization 可以在路径前端做出正确决定;Execution Control 必须把约束带到真正改变现实的最后一跳。

四、Authorization 证明"刚才允许",Execution Control 必须确认"现在仍然允许"

授权判断经常依赖实时上下文:设备状态正常、风险评分合格、余额充足、集群健康、时间窗口有效。这些判断在时刻 t₁ 做出,而真实执行发生在 t₂。

假设 t₁ 时剩余日额度为两万,系统据此允许一笔一万五。在实际执行之前,另一笔并发操作消耗掉一万,t₂ 时剩余额度只有一万,原本合法的动作已经越界。授权在 t₁ 没有做错任何事,失效的是判定的新鲜度。这就是经典的 TOCTOU 问题在自动执行环境中的形态,只不过它的后果从"读到过期数据"变成了"造成越界的现实结果"。

真正需要的不是"所有条件都在执行前重验一遍"——那在工程上既昂贵又不总是可能。需要的是一条区分判据,而这条判据可以给得相当明确:条件是否可能被第三方并发消耗

不可并发消耗的条件,例如主体身份、镜像签名、代码来源、对象是否在注册表中,在 t₁ 与 t₂ 之间不会因为别人的行为而改变,在决策点验证一次通常足够。可并发消耗的条件,例如剩余额度、库存、席位、配额、锁的持有状态、以及任何形式的累计敞口,其取值取决于系统中其他主体在这段时间里做了什么。这类条件必须在执行点重新验证,并且验证与扣减需要是原子的——否则重验只是把竞态窗口缩短,而没有消除它。

同一条判据也适用于状态证据:一份状态证明能覆盖多长的时间窗口,取决于它所描述的量是否可被并发改写,而不取决于签发它的系统有多可信。

Authorization 可以证明刚才允许;Execution Control 必须确认现在仍然允许。

五、Execution Control 不是再授权一次,而是不把最终能力交给上层主体

如果 Execution Control 的实现只是"在调用执行器之前再请求一次授权接口",那它就是一次重复授权,除了增加延迟以外不改变任何安全性质——因为如果上层主体已经持有最终凭据,它完全可以跳过这次调用。

真正的差异必须落在能力的分配上,而不是检查的次数上。这意味着一组结构性约束:上层 Agent 即使身份合法、权限完整,也不直接持有最终执行凭据;编排层即使控制整个业务流程,也无法独自生成一次完整的执行证明;执行器即使具备现实动作能力,也只接受满足特定绑定关系的请求。

执行路径由此从:

Authorized Actor → Direct Execution

变为:

Authorized Request → Execution Qualification → Boundary Verification → Final Execution

关键变化不在于中间多了两个环节,而在于 ALLOW 不再意味着把改变现实的能力交到上层主体手里。上层主体能做的是请求执行(Request Execution),它不能导致执行(Cause Execution)。这两个动词之间的差别,就是本篇要建立的核心区分:前者是一种可以被拒绝的提议,后者是一种既成事实。

这条区分还有一个可以直接用来检查现有架构的推论:如果某个上层组件失陷之后,攻击者能做的仅仅是让系统收到一批被拒绝的请求,那么执行能力是被约束的;如果它失陷之后能够让动作直接发生,那么无论前面有多少层授权,最终执行能力实际上一直在它手上。

Authorization 赋予主体提出请求的资格;Execution Control 保留让请求成为现实的能力,并对它单独设限。

六、Execution Control 要保护的对象是 Decision-to-Execution Integrity

把前面几章合起来,Execution Control 的核心技术对象可以命名为决策到执行的完整性:从安全系统做出允许判断,到现实动作真正发生,这段链路上的执行语义必须保持不变。

完整性维度要求典型失效形态
Action Integrity最终动作不被替换批准的是重启,执行的是重建
Target Integrity最终对象不被替换批准的是测试集群,执行落到生产集群
Parameter Integrity关键参数在判定后不被改写金额、数量、范围在下游被重新构造
State Validity执行时所依赖的状态仍然成立过期额度、过期健康状态被继续使用
Evidence Binding证明属于当前这一次执行复用上一次的批准或状态证明
Credential Constraint最终凭据只能用于满足上述条件的动作凭据本身没有作用域,可用于任意同类调用
Execution Uniqueness一次判定只对应一次执行重试或重放导致同一批准被多次兑现

这张表的每一行都对应一类在授权层完全正确的情况下仍会发生的事故。也正因为如此,Execution Control 的实施位置不能只在决策侧——上述任何一项的最终校验点,都必须位于不可逆动作发生之前,且不受参数构造方控制。

这里还有一类比参数替换更根本的问题:如果那些约束本身可以被链路上的某个主体修改,那么完整性保护就失去了参照系。哪些约束应当属于可变业务策略、哪些应当成为不能被单方面关闭的执行不变量,以及做出判断的那个组件是否天然拥有关闭它们的权力,属于下一篇的范围。

七、授权事故与执行控制事故的检出方式完全不同

区分两者最实用的方式,是看它们各自留下什么痕迹。

授权失败的典型形态是一个本不该被放行的请求被放行了:普通用户触及了管理接口,某个服务账号访问了不属于它的资源。这类事故在事后审查中相对容易识别,因为主体与资源的组合本身就是异常的——它在关系层面就不该出现。

执行控制失败则可以发生在授权完全正确的前提下。授权正确地允许了 Agent-A 对 Target-B 执行 Action-X,而执行链中对象被换成了 Target-C,动作照常完成;或者授权在 t₁ 正确通过,t₂ 执行时关键状态已经变化而执行器没有重验。这两种情况下授权系统都没有做错,错误在于一个曾经正确的允许被转化成了不再正确的现实。

麻烦之处在于痕迹。这类事故在日志里看起来完全正常:每一条记录都是一个合法主体、持有合法凭据、调用了它有权调用的接口,返回成功。没有拒绝,没有异常主体,没有失败重试,没有任何一条记录单独看是可疑的。异常只存在于记录之间的关系里——批准的那条与执行的那条指向了不同的对象。这意味着仅靠事后观察很难发现执行控制失败,也预示着后面必须处理的一个问题:如果日志只能证明事情发生过,那么用什么来参与决定事情能不能发生。

维度AuthorizationExecution Control
核心问题这个请求是否满足允许条件被允许的那件事是否原样抵达现实
作用对象请求判定与执行之间的链路
输出判断约束
典型失效不该被允许的请求被放行已被允许的请求在途中变了语义
事后可见性主体与资源的组合本身异常单条记录全部正常,异常在记录之间

结语:第一阶段的收束

这一篇也走完了本系列第一阶段的最后一步。前三篇分别建立了三组不等式:Trust 不等于 Authority,Privilege 不等于 Execution Authority,Authentication 不等于 Execution Qualification。本篇补上第四组:Authorization 不等于 Execution Control。

四组合在一起,指向同一个结论。访问控制体系可以非常准确地决定谁能够接近某项能力——事实上它在这件事上做得越来越好,动态授权、持续评估、细粒度策略都在持续提高这个判断的精度。但当这项能力开始直接改变现实之后,判断的精度不再自动等于结果的正确性,因为在判断与结果之间存在一段独立的、需要被单独建模的链路。完整的执行链因此从 Authenticate → Authorize → Execute 变为 Authenticate → Authorize → Qualify → Control Execution → Execute:认证建立主体真实性,授权建立操作许可,执行资格建立本次动作的成立条件,执行控制保证这些条件被完整带到真正改变现实的那一点。

需要说清的是,这里没有任何一步是用来取代前一步的。授权仍然负责挡住绝大多数根本不该进入执行链的请求,而且它挡住的数量远超后面几层。Execution Control 处理的是通过了所有前置检查之后剩下的那部分风险——数量上很小,后果上通常最大。

到这里,一个更深的架构问题已经无法回避。前面几篇反复提到"策略返回 ALLOW",却一直把它当作一个既定输入。但策略引擎本身也是系统中的一个主体,它做出判断这件事,和它有能力让判断变成现实这件事,究竟是不是同一件事?如果一个组件既定义规则、又评估规则、还持有把评估结果兑现的凭据,那么它到底是在执行判断,还是本身就是最终权力?这就是下一篇:Policy Engine vs Final Authority

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐