授权系统高可用、迁移与安全退出
某内容平台把集中授权服务部署成三个副本,健康检查全部返回成功,团队因此把它标记为高可用。数据库主库故障后,读流量切到落后副本,刚刚撤销的外包账号重新获得下载许可;写接口却把关系删除报告为成功。系统“可用”了,但正确决策率和撤销目标同时失守。多副本只解决进程存活,不能替代 datastore 一致性、revision 新鲜度与错误语义。
另一家公司迁移关系授权模型时,在请求线程依次写旧库和新库,再同步调用两端 Check。新库偶发超时,接口已经把旧库写成功返回给业务;几天后团队切换到新端,缺失关系集中爆发。紧急回滚又把流量直接送回已停止同步的旧端,把迁移期间的新授权全部丢掉。所谓双写和双读没有单一事实源、幂等日志或追平条件,反而制造了两个都不可信的权威。
高可用首先是决定正确
授权系统的可用性不是“PDP 返回 HTTP 200”。请求必须由允许的 policy/model revision 和满足新鲜度要求的数据求值,PEP 必须实际执行决定,撤销还要在风险目标内抵达所有入口。至少分别定义 PDP 可用性、正确决策率、p95/p99、revision 收敛率、撤销 p99、PEP 覆盖率和审计投递完整率。
一次授权链路通常经过身份上下文、PEP、PDP、策略或模型、PIP/关系 datastore、缓存和决策日志。集中式部署把网络、负载均衡与 datastore 放进关键路径;sidecar 或嵌入式引擎减少网络跳转,却把策略分发、实例盘点和旧 revision 收敛交给平台。选型不是“本地一定快、集中一定稳”,而是明确谁拥有状态、故障在哪个边界传播以及哪些 action 允许何种降级。
先为每类动作写失败合同:高风险写、密钥、管理与导出在 PDP timeout、PIP 错误或新鲜度未知时返回 deny/error;普通低敏只读是否允许短期缓存降级,需要独立审批、最大年龄、同 revision 约束和告警。break-glass 使用强身份、窄 action、短时授权、双人批准与自动回收,不能由 PDP 故障自动触发。
部署一个可演练的授权拓扑
参考拓扑至少包含两个跨故障域 PDP 副本、健康与就绪探针、受支持的高可用 datastore、独立的策略控制面和有界审计出口。客户端使用短 timeout、整体 deadline 和仅针对幂等 Check 的有限重试;关系写入、模型创建和策略发布携带 operation ID,不能盲重试。
部署配置不应写死浮动标签。镜像使用经过验证的版本与 digest,凭证由工作负载身份或 Secret 系统注入,配置仓只保存变量名。下面的环境文件展示应冻结的运行参数,值由环境配置系统提供:
AUTHZ_IMAGE=registry.example.invalid/authz-pdp@sha256:<verified-digest>
AUTHZ_REPLICAS=3
AUTHZ_REQUEST_TIMEOUT_MS=80
AUTHZ_MAX_RETRIES=1
AUTHZ_MAX_POLICY_AGE_SECONDS=300
AUTHZ_AUDIT_QUEUE_CAPACITY=20000
AUTHZ_DATASTORE_URI_FILE=/run/secrets/authz-datastore-uri
AUTHZ_SIGNING_KEY_FILE=/run/secrets/policy-verify-keyAUTHZ_REPLICAS 提供并发和故障域冗余,不保证数据正确;REQUEST_TIMEOUT 必须给业务处理保留预算;MAX_RETRIES 只覆盖可判定的瞬时错误;MAX_POLICY_AGE 决定控制面断开后旧策略还能服务多久;审计队列容量决定日志后端故障时的缓冲窗口。高风险行为在缓冲耗尽后继续还是阻断,必须按合规等级预先决定。
上线步骤是先部署 datastore 与备份,再部署无流量 PDP,加载批准 revision,执行合成 allow/deny/error 和撤销检查,确认 readiness 返回 active revision 与数据依赖状态,最后按故障域逐步接流。OPA sidecar 的 bundle 激活、持久化和 status 可参考 OPA Bundle Management;集中式关系授权需按所用产品的生产部署指南配置数据库与网络,而不是把内存 datastore 当作 HA。
故障语义要比健康检查更具体
PDP timeout 或 5xx 默认映射为依赖错误,不得伪装成“没有关系”或普通 deny 后被其他 permit 覆盖。PIP/关系库不可达同理:未知事实是 Indeterminate,不是 false。新策略激活失败时继续使用上一批准 revision,并暴露激活错误和旧版本年龄;超过允许年龄后,高风险 action 摘流或失败关闭。
failure_contract:
pdp_timeout:
high_risk: indeterminate_then_deny
low_risk_read: approved_cache_only
pip_unavailable:
all: indeterminate
candidate_activation_failed:
serve: last_approved_revision
reject_high_risk_after_max_age: true
audit_sink_unavailable:
buffer: bounded
expose_queue_depth_and_drop_count: true正向演练先对同一请求连续访问各副本,预期得到相同 decision、revision 与 freshness evidence;再停止一个 PDP,负载均衡应摘除它,其余副本保持预算内服务。反向演练让 datastore 不可达,预期高风险写不产生业务副作用,日志明确区分 dependency error 与 policy deny。若任一副本使用旧 revision 返回 allow,不能用多数投票掩盖,因为一个旧 allow 就足以形成泄漏。
健康检查分两层:liveness 只判断进程能否恢复,readiness 检查是否已装载允许的 revision、关键依赖是否满足服务等级。不要让暂时的 datastore 抖动触发所有进程重启,也不要让尚未收敛策略的副本进入流量。
灾备从 RPO、RTO 和撤销目标拆解
授权 datastore 的灾备不能只备份“当前关系”。恢复包还需要 policy/schema/model、不可变 ID、租户与 store 映射、关系或实体数据、删除墓碑、条件参数、变更日志、PEP action 映射、加密与签名密钥引用。RPO 决定可能丢失多少授权变更,RTO 决定恢复服务耗时,REO 决定撤销最迟何时在所有 PEP 生效;三者是不同目标。
备份应加密、按租户和环境隔离、限制读取,并用独立身份执行。恢复演练在隔离环境完成:先恢复 datastore 和不可变模型,再加载策略 revision,校验分片总数与摘要,重放黄金 Check/List/撤销语料,最后验证旧凭证和旧网络入口不可访问。只验证“备份文件能解压”不能证明授权语义恢复。
跨区域切换前确认目标区域的数据位置满足所需一致性。只读副本可能足以服务低风险读,却不一定能证明刚撤销后稳定 deny。OpenFGA 的 consistency 取舍可查看 OpenFGA Consistency;SpiceDB 需要根据 SpiceDB Consistency 选择 ZedToken、at_least_as_fresh 或更强读取,而不是把所有读都当作同一种“主从切换”。
三类变更不能绑成一次切换
授权升级通常同时涉及引擎版本、policy/schema/model 和授权数据。三者故障面不同,应拆开验证。引擎升级关注解析、求值、API、错误码、性能与 datastore migration;policy/model 变化关注语义差异和不可变 revision;数据迁移关注完整性、顺序、幂等、新鲜度和删除传播。
OPA/Rego 或 Cedar 的策略制品应带引擎兼容信息、摘要与签名,并以原子指针激活。OpenFGA model 创建后以不可变 ID 固定请求,不能依赖“最新 model”;SpiceDB schema 变化先做 validate 与 diff,遵循版本对应的 datastore migration 兼容窗口。不要直接降数据库 schema 来回滚应用版本。
一次发布只改变一个主要变量:先让旧策略在新引擎上通过单元、回放、影子与性能验证,再部署引擎;随后发布新 policy/model;最后迁移数据形态。把三者一起切换会让 deny -> allow 无法归因,也会使回滚找不到兼容组合。
双写必须由单一意图日志驱动
迁移期间的权威事实来自业务事务与 outbox/意图日志,不来自两个授权库谁最后写成功。API 在同一业务事务中提交资源变更和授权意图,幂等消费者分别写旧端与新端;每端记录 operation ID、业务版本、目标状态和 revision。请求线程不能做无事务保障的“先旧后新”。
{
"operation_id": "op-opaque-42",
"tenant": "tenant-a",
"entity_key": "document:9#viewer@user:17",
"operation": "delete",
"business_version": 42,
"old_target": { "status": "applied", "revision": "old-r17" },
"new_target": { "status": "pending", "revision": null }
}重复 delete 应保持成功,旧业务版本不能覆盖新版本。乱序事件按稳定 sequence 或业务版本拒绝倒退,不能用两端 wall clock 决胜。条件、Caveat 或 context 无法等价映射时停止自动迁移:保留旧 PEP,或把属性策略与关系授权按固定顺序组合,绝不能用“近似 allow”追求零差异。
安装迁移 worker 后先以只读凭证扫描源数据并生成计数、分组摘要和预计容量,再使用短期、可撤销的写入身份回填目标端。目标凭证只能访问指定 tenant/store,日志不得包含 datastore URI、token、完整关系条件或真实主体。worker 的并发、批大小、重试、死信和限速独立配置,避免回填压垮权威 PDP。
双读双判不等于双主执行
在旧端保持执行权威时,PEP 同步调用旧 PDP,把规范化请求异步复制给新 PDP。比较器记录旧新 decision、reason、model/schema revision、freshness mode 和延迟。新端只产生影子结果,不执行 obligation,不写业务状态,也不改变权威缓存。
business request
-> PEP -> old PDP -> authoritative decision -> enforce
\
-> bounded shadow queue -> new PDP -> compare only先比较单项 Check,再比较 ListObjects/ListUsers/Lookup 集合。集合比较固定分页 snapshot、去重与排序;任一侧新鲜度未知或 revision 不同则标记不可比。差异按 allow -> deny、deny -> allow、same -> error、reason 与延迟分类,未解释的权限扩大阻断切换。
切换执行的最小单位是 tenant + action + resource type。先选低风险、低流量、关系形态简单且可回滚的单元;每次灰度仍保留旧端影子核对。高风险撤销同时写两端后,分别验证稳定 deny 和 REO,不能只比较最终布尔值。
用迁移状态机约束前进和回退
迁移可拆成七个阶段。M0 冻结黄金语料、REO、模型和导出清单;M1 回填新端,旧端仍读写;M2 由意图日志驱动双写,旧端仍权威;M3 影子双判;M4 按最小单元灰度到新端;M5 新端权威、旧端继续接收兼容写并做影子核对;M6 停旧写并保留只读核对窗。
migration_gate:
source_of_truth: business_outbox
write_lag_seconds_max: 5
pending_operations: 0
unexplained_deny_to_allow: 0
same_to_error: 0
revoke_p99_within_reo: true
list_set_diff_unapproved: 0
rollback_route_tested: true示例阈值需要按业务流量和数据规模调整。阶段退出条件必须有证据:全量导入比较总数、按类型/关系分组计数与摘要;写入 lag、失败重放与死信归零;影子差异完成审批;p99 与容量不过载;回切演练确认旧端追平。没有退出条件的“观察几天”不是状态机。
新模型和旧模型都保留不可变 ID。流量路由记录执行权威,避免同一租户随机落到两个模型。迁移 owner、业务 owner、安全审核、数据库 owner 与值班职责写入变更单;能推动阶段的人不能独自删除审计或绕过差异门禁。
正反实验验证迁移不会制造越权
正向实验在隔离租户创建 document:9#viewer@user:17,让旧端稳定 allow。提交带 operation ID 的关系写入,等待新端回填并验证两端 revision;影子 Check 应同为 allow。随后按最小灰度单元切到新端,业务副作用计数只增加一次,decision ID 能关联执行权威。
接着删除 viewer。记录权威提交时刻、两端写入证据和新鲜度模式,循环 Check 到两端连续稳定 deny;预期 p99 在该 action 的 REO 内,长连接和缓存也重新授权。这个实验同时证明删除传播、缓存键、双写幂等和 PEP 执行。
反向实验依次注入新端写超时、重复事件、乱序旧版本、影子 PDP 500、List 分页跨 snapshot 和旧端同步暂停。预期请求线程不会谎报双端成功,重复操作不产生额外关系,旧版本不能复活权限,影子故障不阻塞权威路径,不可比集合不进入一致率,旧端 lag 超标会阻断回切。
expected evidence:
operation op-opaque-42 -> old=applied,new=applied
duplicate op-opaque-42 -> deduplicated
business_version 41 after 42 -> rejected_as_stale
new PDP timeout -> shadow_error, old decision enforced
revoke -> stable deny on both sides within REO执行环境才会产生真实退出码、延迟和日志;正文中的结果是判定合同。保存工具版本、镜像 digest、输入摘要、revision、操作状态、实际输出和清理结果,不能把示例文本当作生产基线。
回滚要先证明旧端仍可信
M1 到 M3 的回滚可以停止新端 consumer 与影子读,因为旧端一直是唯一权威。M4 到 M5 不能直接把路由送回旧端:先确认旧端已经追平切换期间的全部变更;若 lag 超标,冻结高风险授权变更或从意图日志重放,直到旧端可证明可信。
policy/model 回滚通过不可变 revision 路由完成,不删除候选版本。OPA 或 Cedar 回指上一签名 bundle;OpenFGA 请求重新 pin 旧 model ID;SpiceDB schema 与 datastore migration 按其兼容窗口处理,不能用手工 DDL 猜测回退。回滚后重新跑黄金 Check/List/撤销语料,并确认所有 PEP 与缓存键都使用旧 revision。
灾难回滚还要处理已发出的副作用。授权决定本身可回切,不代表已下载的数据、已生成的预签名 URL 或已建立的长连接自动失效。PEP 维护副作用清单,回滚流程终止连接、撤销临时凭证、清除受影响缓存并通知数据 owner。
安全退出需要可移植证据包
退出包保存所有租户的 policy/schema/model 源文件、不可变 ID、引擎版本、制品摘要与签名;全量关系/实体、分页 cursor 处理、总数与分片摘要、删除墓碑和条件数据;PEP endpoint 到 action 映射、tenant/store 映射、上下文来源、缓存键与一致性策略;以及脱敏黄金语料、迁移意图日志、失败重试和最后同步 revision。
OpenFGA model 不可变且 model ID 参与请求,必须保存全部仍在使用的 model 与 store 映射,不能只导出“最新模型”。SpiceDB 大批量数据迁移使用官方 Bulk Export/Import 并处理流式分页、重试与计数,可从 SpiceDB Bulk Operations 确认接口语义。OPA/Cedar 还要导出控制面元数据,只有策略源码无法证明哪个 revision 曾在何处生效。
安全顺序是冻结目标版本、全量导出、持续追平变更日志、导入新环境、比较计数与摘要、重放 Check/List/撤销、影子双判、切换执行、停旧写、保留只读核对窗,最后撤销客户端凭证和网络入口。审计与 legal hold 按保留要求处理,过期后再销毁数据、备份和密钥。
decommission 完成的证据不是“旧控制台打不开”,而是旧 PEP 路由为零、旧写入停止、未决 operation 为零、客户端凭证已撤销、网络策略已关闭、日志和备份有处置记录、恢复演练不能再用旧身份获得服务。退出演练应在采购或自建之初完成,因为无法导出模型、关系和审计的系统会把迁移成本推迟到最危险的时刻。
容量、成本与长期职责
容量模型分开统计 Check QPS、List/Lookup QPS、关系写入速率、关系总量、策略或实体快照大小、图遍历深度与扇出、PIP 调用、缓存命中、审计事件和撤销事件。迁移期还要加上双写、影子双判、全量回填、摘要计算和旧端保留,峰值资源可能远高于稳态。
成本不仅是 PDP 节点与数据库,还包括跨区流量、备份、日志索引、密钥系统、策略评审、模型迁移、PEP 改造、值班、培训和退出演练。嵌入式授权把控制面和观测成本转给平台,托管服务也不会替业务修复 action 映射、旁路入口和错误语义。
平台团队维护运行时、分发、备份、SLO 和迁移工具;业务团队拥有 action、资源语义与 PEP;安全团队审核权限扩大、break-glass 和日志边界;数据 owner 决定保留与跨区策略。每次引擎升级、模型变更和 datastore 维护都要有独立 owner、回滚 revision、兼容矩阵和演练证据。
从异常现象定位故障层
“副本都健康但同一请求结果不同”先按 policy/model revision 与 data snapshot 分组,再检查输入规范化、PIP 来源和缓存键。不要用多数投票修正授权正确性。“关系已删除仍 allow”按源提交、意图日志、两端 apply、PDP/网关/SDK 缓存和长连接重新授权逐层定位,保留旧 allow 证据,不要先重启全部节点。
“迁移回滚后权限丢失”核对旧端是否在灰度期间持续接收兼容写、outbox lag 和死信是否归零、回切前是否冻结高风险变更。“恢复成功但 List 与 Check 不一致”检查模型 ID、分页 snapshot、条件数据、后过滤和索引构建状态;总数相等不能证明关系语义相等。
“审计正常但发生越权”优先查 PEP 清单、批量和后台入口、错误映射、直接存储链接与旧长连接。PDP HA 无法保护一个没有执行策略的入口。修复必须回到窄授权网关、稳定 action 映射和副作用不变量,而不是只增加授权服务副本。
SLO 同时覆盖正确决定、延迟、revision 收敛、撤销、PEP 与审计,不以 HTTP 200 代替可用。PDP 跨故障域部署,readiness 验证批准 revision 与依赖状态,幂等 Check 才允许有限重试。datastore 故障、新策略失败、日志积压和旧版本超龄都有明确 deny/error/降级合同。
备份包含模型、关系、墓碑、映射、变更日志与密钥引用,并通过隔离恢复和语义重放。引擎、policy/schema/model 和授权数据分开升级,每个阶段有兼容组合与回滚 revision。双写由业务 outbox/意图日志驱动,操作幂等、有序、可重放,旧新端不是双主。
双判使用相同规范化输入和可比较新鲜度,新端影子结果不产生业务副作用。灰度以租户、action 和资源类型为最小单位,回切前证明旧端已经追平。退出包可导出、校验和重放;旧凭证、网络入口、缓存、日志与备份都有最终处置证据。
