策略运行时与策略工程
发布窗口里,团队把一条“只有订单 owner 才能退款”的规则从应用代码移进策略文件。单元测试全绿,OPA 也返回了 allow: false,线上却仍发生越权退款。追查请求链才发现,应用把“OPA 调用失败”和“明确拒绝”合并成同一个异常分支,异常分支沿用了旧逻辑继续执行;策略做出了正确决定,执行点却没有落实它。另一个服务把 undefined 当成空 JSON 后使用默认值 true,缺少字段的请求同样被放行。事故不在某一行 Rego,而在 input、决策、错误语义与业务动作之间断了链。
另一次问题出现在配置流水线。仓库中的 Kubernetes YAML 通过了 Conftest,部署后却没有任何副本;开发者以为策略漏检,最终发现 CI 读取的是 Helm 模板源码,而策略假设 input.kind 已经是渲染后的 Deployment。随后有人全局打开 --combine,input 又变成带 path 和 contents 的数组,旧规则因此一次也没有命中。工具退出码为零、策略能编译、文件能解析,都不能证明预期对象被检查过。
先分清谁决定、谁执行
OPA 的核心角色是 Policy Decision Point,简称 PDP。调用它的应用、网关、代理或流水线是 Policy Enforcement Point,简称 PEP。PEP 收集主体、动作、资源和环境,构造成结构化 input;OPA 将 input 与常驻 data、Rego policy 一起求值;PEP 再把布尔值、对象、集合或未定义结果解释成真实动作。
调用现场 -> PEP 构造 input -> PDP 求值 policy + data
<- decision / undefined / error <-
PEP 执行允许、拒绝、过滤、告警或停止流水线这条链上的责任不能互相替代。OPA 不认证用户,不替应用执行退款,也不知道一次超时应该开放还是关闭;应用不能只凭 HTTP 200 判断授权成功,因为 200 只说明查询已完成;流水线也不能把“零条规则命中”解释为配置合规。每个接入都要固定 decision path、返回类型、超时预算、缺失字段行为、未定义行为和失败策略。
Rego 是表达决策的语言,不是一个单独的授权服务器。Conftest 则把 YAML、JSON、HCL、Dockerfile 等静态配置解析为 input,运行 Rego 并以退出码反馈给本地或 CI。它适合把问题提前到提交阶段,但不会替 Kubernetes schema、Terraform provider 或目标程序验证运行语义。
五种运行路径对应五种故障方式
本地 CLI 先证明策略语义
opa fmt、opa check、opa eval 和 opa test 适合开发与 CI。它们启动快、输入可固定、错误容易复现,是新规则的第一站;它们不是常驻服务,也不能证明网络调用、进程资源和故障退化正确。
opa fmt --fail policy/
opa check --strict policy/
opa test --coverage --format=json policy/
opa eval -d policy/ -i fixtures/allow.json 'data.orders.authz.decision'应用内 SDK 与 Wasm 追求低延迟
Go 应用可以使用 OPA 的 /v1/rego 或 /v1/sdk 包在进程内求值,省去网络跳转,并与应用一起扩缩容。代价是策略更新、内存、CPU、崩溃和升级都进入应用故障域。Wasm 会把指定 entrypoint 编译成 bundle,由宿主加载执行,适合非 Go 进程、边缘节点或浏览器侧的确定入口;宿主仍要实现 ABI、数据更新、错误映射以及不受原生支持的 built-in,Wasm 也不会自动获得 OPA 的 HTTP 与管理能力。
sidecar 或节点代理把决定放到请求旁边
sidecar 通过 loopback 提供稳定 HTTP 接口,网络延迟低,外部网络短时中断时还能使用已加载策略。它会让每个工作负载都持有策略和 base data 副本,实例数量、内存与升级批次随业务增长。节点代理减少副本,却扩大单实例故障影响,并要求可靠隔离不同租户的请求、缓存与日志。
共享服务集中资源也集中风险
共享 OPA 服务适合大数据集、批量决策或多语言统一接入。它把策略与资源集中起来,同时引入网络延迟、队列、限流和共享故障域。必须独立计算 P99、峰值并发、重试放大、策略更新期间的版本一致性,以及服务不可达时各业务动作应该拒绝、降级还是暂停。
Conftest 把静态错误挡在发布前
Conftest 适合 pre-commit、PR 和 CI,输入是文件解析结果,不是运行请求。应明确检查模板源码、渲染结果还是两者;固定 parser、namespace、策略目录和输出格式;用违规 fixture 证明规则真的会失败。对于跨文件关系,--combine 会把输入改成数组,必须为新形状单独写测试,不能把它当作透明开关。
用共同契约组织策略仓库
一个可维护的策略仓库至少把模块、测试输入、辅助数据和接入契约分开:
policy/
orders/authz.rego
orders/authz_test.rego
data/
roles.json
fixtures/
allow.json
deny-wrong-owner.json
deny-missing-subject.json
contracts/
decision.schema.jsoninput 由 PEP 在每次调用时提供,适合放已验证的主体、动作、资源属性、租户和请求上下文;data 是 base documents 与 Rego 计算出的 virtual documents,适合放角色映射、产品配置和可批量更新的属性。不要把瞬时请求偷偷写进共享 data,也不要在策略内通过网络请求拼装大量外部状态,否则一次授权会继承 DNS、TLS、超时、缓存和外部服务的全部不确定性。
决策最好返回结构化对象,而不是只返回布尔值。例如 { "allow": false, "reason": "owner_mismatch", "policy_version": "..." } 能帮助 PEP稳定映射错误、关联审计与比较新旧策略。结构化结果不能包含敏感原值,也不能让客户端根据可枚举原因推断资源是否存在。
正例与反例必须成对存在
策略测试不只证明允许路径。对一个订单读取规则,最小组合应包括 owner 读取成功、其他用户被拒绝、动作拼写错误被拒绝、缺少 subject 被拒绝、资源类型错误被拒绝、辅助 data 缺失时稳定失败,以及冲突规则无法编译或求值。
package orders.authz
default allow := false
allow if {
input.action == "read"
input.subject.id == input.resource.owner_id
}先执行允许 fixture,再执行错误 owner 与缺字段 fixture。若删除 default allow := false 后结果变成 undefined,适配层必须仍然拒绝;若添加两个对同一 complete rule 返回不同值的规则,测试应看到 conflict,而不是任选其一。Conftest 同样需要一份明确违规的配置并断言非零退出,防止 namespace、parser 或 input 形状错误造成“什么都没检查”。
排障从第一份分层证据开始
| 现象 | 第一份证据 | 常见原因 | 修复后的再验证 |
|---|---|---|---|
OPA 返回 200 但业务拒绝 | decision path、响应 result 与 PEP 映射日志 | 查询路径错误、返回类型不符、规则为 undefined | 同一 request ID 下比对原始结果与业务动作 |
| 本地允许、服务调用拒绝 | 两侧 OPA 版本、policy/data revision、完整 input 摘要 | 实例策略未收敛、data 不同或输入序列化变化 | 固定 revision 后重放同一脱敏 fixture |
| CPU 突增 | query metrics、热规则、input/data 规模 | 全表扫描、嵌套遍历、外部 built-in 或日志 mask 过重 | 用代表性分布比较 P50/P95/P99 与分配量 |
| Conftest 意外通过 | JSON 输出、parser、namespace、测试数量 | 规则未命中、读取了模板而非渲染结果 | 违规 fixture 必须产生目标 deny |
| 容器外无法访问 OPA | 实际监听地址与 socket | OPA 1.x 默认监听 localhost | 仅在完成网络与 API 加固后显式绑定 |
不要从“规则看起来正确”开始猜。先保存脱敏 input 摘要、decision path、决策类型、策略版本、data 版本、耗时、OPA 实例和 PEP 最终动作。授权错误经常出在调用与执行之间,仅看 Rego 会错过真正断点。
性能预算取决于数据位置
性能测试要使用接近生产分布的 input 和 data,并分开冷启动、策略编译、数据更新与稳定求值。只测一条简单规则的平均值,会掩盖集合扫描、对象复制、GC、并发队列和长尾。通过 opa eval --metrics、基准测试或宿主埋点记录求值耗时与分配量,再把 PEP 序列化、网络和重试纳入端到端预算。
本地 SDK 和 Wasm 通常延迟最低,但每个进程复制数据;sidecar 增加本地 HTTP 与资源副本;共享服务减少副本,却增加网络与排队。数据很大、更新频繁时,不要只问“哪种更快”,还要计算副本总内存、更新风暴、旧策略可接受时长与扩容速度。
API、凭证与日志按敏感资产治理
OPA 远程服务默认不能被当成已有安全边界。PEP 通常只需读取固定决策路径,不应获得 Policy API 或 Data API 写权限。把监听地址改为 0.0.0.0 时,要同时配置 TLS、客户端身份、system.authz、网络策略和管理端点隔离;健康检查与 metrics 也可能暴露版本、插件和内部状态。
input、data 与决策日志可能包含用户标识、资源属性、请求头和业务结果。日志要在 OPA 侧先做最小化和遮盖,再传给受限存储;采样、丢弃和缓冲溢出会制造审计缺口,不能把 decision log 当作绝对不丢的事务日志。策略仓库、bundle 下载凭证、签名公钥和 CI token 应由各自 owner 管理,私钥不能进入 bundle 或消费端配置。
高可用不是简单增加副本
本地 CLI 没有在线 HA 问题;应用内、Wasm 与 sidecar 随业务副本扩展,外部服务失联时仍可能使用最近成功加载的策略,但旧策略能使用多久必须有业务约束。共享服务需要多副本、负载均衡、超时、限流和故障隔离,还要避免客户端重试把故障放大。
策略一致性同样重要。多副本都 Ready,不表示加载了同一 revision;发布后应比较活跃版本、激活错误和真实决策。高风险动作通常 fail-closed,低风险只读动作是否允许短时使用旧策略,要由业务损失、撤权时效和依赖可用性共同决定。把所有请求统一 fail-open 会把策略服务故障直接转换成越权事故。
升级与退出先保留可逆路径
OPA 1.x 默认采用 Rego v1,新规则使用 if 和 contains。旧语法兼容开关只适合迁移过渡。升级时先在固定版本的 CLI 与 CI 中运行格式、严格检查、单元测试和正反 fixture,再影子比较新旧运行时对同一批脱敏输入的结果;最后分批切换 PEP,并保留旧二进制、旧 bundle 与回退开关。
Conftest 升级要同时固定其自身版本和内嵌 OPA 语义,回归 parser 输出、namespace、warning 退出码与 combine fixture。退出某种运行形态时,先让 PEP 改读新决策源,证明允许、拒绝、超时与未定义一致,再停止旧实例,删除端口、凭证、缓存、临时策略和 CI 下载项。最后检查仓库、镜像缓存与流水线中没有遗留旧镜像或兼容参数。
策略工程真正提高效率的标志,不是把 if 从业务代码搬到 .rego,而是团队可以解释每次决定使用了什么输入和版本、在哪里执行、失败时怎样退化、如何复现、怎样升级以及如何完整撤销。只要这条链中还有一个默认值靠猜,策略就还没有成为可靠的工程能力。
