OPA 与 Rego v1 从决策建模到多运行时接入
退款接口接入 OPA 后,应用团队看到了稳定的 HTTP 200,便在监控里把它记成“授权成功”。一次策略重构把查询路径从 data.orders.authz.decision 改成了 data.orders.authz.allow,调用端却没有同步。OPA 对合法查询返回 200,响应中没有 result;适配器把空值转成语言默认布尔值,随后继续执行退款。问题不是 OPA 没有拒绝,而是团队从未定义“未找到决定”应该怎样进入业务状态机。
另一个现场中,同一策略在开发机允许,在共享环境持续拒绝。两边源码提交一致,排查几小时后才发现共享实例加载了旧 data,开发机则通过 -d . 同时读入了新角色映射;调用端日志只记了 allow,没有记录 decision path、policy revision、data revision 和脱敏 input。没有这些证据,“策略一样”只是对文件的判断,不是对一次求值的判断。
OPA 只负责算出决定
Open Policy Agent 是通用策略引擎。PEP 把主体、动作、资源和环境整理为 JSON input,OPA 用 Rego policy 与 base data 求值,并返回 JSON 决定。决定可以是布尔值,也可以是包含原因、过滤条件或补丁的对象;OPA 不替应用认证主体,不执行退款,也不自动决定超时后开放还是关闭。
subject + action + resource + context
|
v
PEP 构造 input
|
v
OPA: policy + input + data
|
v
decision / undefined / error -> PEP 执行业务动作本篇实验使用 orders 服务。只有资源 owner 能读取订单,support 角色在工单存在时也能读取;任何未知动作、缺少主体、错误资源类型和未定义结果都拒绝。这个模型刻意保留 data 依赖,便于观察 input 与 data 的差异。
安装时固定可追溯版本
OPA 官方提供 release 二进制、Homebrew 与 Docker 镜像,入口见 OPA 文档首页。团队基线应锁定审核过的 OPA 1.x 版本并校验摘要,不把 latest 或 edge 作为可重复构建输入。安装完成后先记录实际版本:
$version = "<opa-version>"
Invoke-WebRequest `
-Uri "https://openpolicyagent.org/downloads/$version/opa_windows_amd64.exe" `
-OutFile ".\opa.exe"
Invoke-WebRequest `
-Uri "https://openpolicyagent.org/downloads/$version/opa_windows_amd64.exe.sha256" `
-OutFile ".\opa.exe.sha256"
Get-FileHash .\opa.exe -Algorithm SHA256
.\opa.exe version下载页提供的摘要必须与 Get-FileHash 输出匹配;不匹配时停止使用并重新确认下载源、代理缓存和目标架构。macOS 可用 brew install opa 快速进入本地学习,但团队流水线仍要记录 opa version 与包来源。Linux 可下载对应架构的静态二进制,赋予执行权限后放入受管理的工具目录。
Docker 适合隔离验证:
docker pull openpolicyagent/opa:<opa-version>
docker image inspect openpolicyagent/opa:<opa-version> --format '{{json .RepoDigests}}'
docker run --rm openpolicyagent/opa:<opa-version> version正式镜像锁定 digest,升级通过显式 PR 完成。OPA 1.x server 默认监听 localhost:8181;容器若要从宿主访问,需要显式 --addr=0.0.0.0:8181,但扩大监听必须与 TLS、客户端身份、API 授权和网络隔离一起实施,不能只为“端口通”而裸露管理 API。Docker 部署说明给出了镜像入口与启动方式。
Rego v1 用规则描述结果
在项目中新建 policy/orders/authz.rego:
package orders.authz
default decision := {
"allow": false,
"reason": "default_deny",
}
decision := {
"allow": true,
"reason": "owner",
} if {
valid_request
input.subject.id == input.resource.owner_id
}
decision := {
"allow": true,
"reason": "support_ticket",
} if {
valid_request
"support" in data.roles[input.subject.id]
data.tickets[input.resource.id].open == true
}
valid_request if {
input.action == "read"
input.resource.type == "order"
is_string(input.subject.id)
is_string(input.resource.id)
is_string(input.resource.owner_id)
}OPA 1.x 默认采用 Rego v1 语义。规则体使用 if;集合增量规则使用 contains。import rego.v1 在 1.x 是 no-op,不是打开新语法的开关。旧写法 allow { ... } 在默认模式下应被解析或严格检查拒绝,兼容参数只用于迁移,不应成为新策略模板。Rego 语言说明与 1.0 升级指南应作为升级前核对入口。
这里选择结构化 decision,让 PEP 可以记录稳定 reason。两条 complete rule 若对同一输入同时成立且返回不同对象,会产生 eval_conflict_error。示例通过 owner 与 support 条件尽量避免重叠;生产策略还应为所有可能重叠的条件写冲突测试,不能期待 OPA 按文件顺序选择一个结果。
input 与 data 是两种生命周期
新建 data/roles.json:
{
"roles": {
"alice": ["customer"],
"sam": ["support"]
},
"tickets": {
"order-100": { "open": true }
}
}input 是一次求值的请求上下文,只在本次查询中存在。主体标识应由已完成认证的 PEP 写入,不能直接信任客户端随请求提交的 subject。data 包含加载到 OPA 的 base documents,以及由 Rego package 形成的 virtual documents;示例中的角色和工单状态是 base data,data.orders.authz.decision 是 virtual document。
不要用 data 充当业务数据库。角色映射小、更新可批量传播时适合随 bundle 或 SDK 更新;工单状态若高频变化、必须读后立即生效,把它复制到每个 OPA 实例会产生陈旧窗口。可选择由 PEP 先读取权威状态并放入 input,或者使用集中决策服务统一访问。直接在 Rego 中调用外部 HTTP 会把 DNS、TLS、超时、重试和外部可用性塞进授权热路径,只有在明确预算和缓存语义后才采用。
CLI 跑通第一个正向决定
创建 fixtures/owner-read.json:
{
"subject": { "id": "alice" },
"action": "read",
"resource": {
"type": "order",
"id": "order-100",
"owner_id": "alice"
},
"context": { "request_id": "req-owner-read" }
}格式化、严格检查并求值:
opa fmt --fail policy/
opa check --strict policy/
opa eval \
-d policy/ \
-d data/roles.json \
-i fixtures/owner-read.json \
'data.orders.authz.decision'预期 result[0].expressions[0].value 中出现 allow: true 和 reason: owner。项目脚本通常应固定 --format=json 后解析值,分别判断“查询执行失败、undefined、allow=false、allow=true”。--fail-defined 的语义是在查询有结果时返回非零,适合“禁止出现结果”的检查,不适合作为这里的成功退出契约;不要只依赖 shell 退出码推断业务授权。
更直接的自动化命令可以输出原始值:
opa eval --format=raw \
-d policy/ -d data/roles.json \
-i fixtures/owner-read.json \
'data.orders.authz.decision.allow'它应输出 true。若输出为空,先检查 package、decision path 和规则是否 undefined;若命令报类型或冲突错误,保存错误码与规则位置,不要把它转换为允许。
反向实验固定默认拒绝
创建错误 owner:
{
"subject": { "id": "mallory" },
"action": "read",
"resource": {
"type": "order",
"id": "order-100",
"owner_id": "alice"
}
}用同一命令求值时,预期是 allow: false 与 reason: default_deny。随后按顺序做四个破坏实验:
删除 subject.id,应稳定拒绝,证明缺字段不会开放。把 action 写成 Read,应稳定拒绝,证明动作枚举大小写不被猜测修正。临时删除 default decision,未知输入将得到 undefined,PEP 仍必须拒绝。
增加另一条同样匹配 owner、但返回 allow: false 的 complete rule,应出现 conflict,而不是得到不确定结果。
为它们写 Rego 测试:
package orders.authz_test
import data.orders.authz
test_owner_allowed if {
result := authz.decision with input as {
"subject": {"id": "alice"},
"action": "read",
"resource": {"type": "order", "id": "order-100", "owner_id": "alice"},
}
result.allow == true
result.reason == "owner"
}
test_wrong_owner_denied if {
result := authz.decision with input as {
"subject": {"id": "mallory"},
"action": "read",
"resource": {"type": "order", "id": "order-100", "owner_id": "alice"},
}
result.allow == false
}
test_missing_subject_denied if {
result := authz.decision with input as {
"action": "read",
"resource": {"type": "order", "id": "order-100", "owner_id": "alice"},
}
result.allow == false
}执行 opa test --coverage --format=json policy/ data/roles.json。成功特征是三个测试通过且 JSON 中有预期测试名;coverage 用于发现未经过的规则,不等于语义正确。再暂时把拒绝断言改成允许,确认测试确实失败,然后恢复。这一步能识别“测试文件未被发现”造成的假绿。
HTTP 服务要区分状态码与决定
本地启动:
opa run --server --log-format=json policy/ data/roles.json
curl -sS http://localhost:8181/health默认只监听 localhost。健康端点成功只说明进程响应,不说明目标策略已加载、调用路径正确或授权结果符合预期。发送决策请求:
curl -sS \
-H 'Content-Type: application/json' \
--data @fixtures/owner-read.json \
http://localhost:8181/v1/data/orders/authz/decisionREST Data API 期望请求体通常是 {"input": ...},直接把 fixture 作为顶层 body 会导致 input 缺失。更稳妥的文件应写成:
{
"input": {
"subject": { "id": "alice" },
"action": "read",
"resource": { "type": "order", "id": "order-100", "owner_id": "alice" }
}
}再发送该文件,响应 result.allow 应为 true。错误路径可能仍返回 200 但没有 result,因为查询本身有效而文档未定义;语法、编译或某些 API 错误则使用不同状态码。适配器必须同时检查 HTTP 状态、响应能否解析、result 是否存在、类型是否正确和 allow 是否严格为 true,其余情况统一进入明确的拒绝或业务降级分支。
服务化时不要让业务调用身份拥有写入 /v1/policies 或 /v1/data 的能力。OPA 默认 authentication 与 authorization 都是 off;一旦对远程网络监听,应按 OPA Security 配置 TLS、客户端身份、system.authz 和网络边界。TLS authentication 不会自动关闭明文 listener,实际监听 socket 也要检查。
Go SDK 把求值嵌入应用
Go 新项目使用带 /v1/ 的包路径。rego 包适合显式准备查询,sdk 包适合消费动态策略配置。下面展示 rego 的关键结构:
package authz
import (
"context"
"fmt"
"github.com/open-policy-agent/opa/v1/rego"
)
func Evaluate(ctx context.Context, module string, input any) (bool, error) {
query, err := rego.New(
rego.Query("data.orders.authz.decision"),
rego.Module("orders.authz.rego", module),
).PrepareForEval(ctx)
if err != nil {
return false, fmt.Errorf("prepare policy: %w", err)
}
results, err := query.Eval(ctx, rego.EvalInput(input))
if err != nil {
return false, fmt.Errorf("evaluate policy: %w", err)
}
if len(results) != 1 || len(results[0].Expressions) != 1 {
return false, fmt.Errorf("undefined or ambiguous decision")
}
decision, ok := results[0].Expressions[0].Value.(map[string]any)
if !ok {
return false, fmt.Errorf("unexpected decision type")
}
allow, ok := decision["allow"].(bool)
return ok && allow, nil
}准备查询应在启动或策略更新时完成,不要每个请求重复 parse/compile。真实项目还要注入 base data、使用带 deadline 的 context,并把错误映射为稳定业务结果。不要记录完整 input;记录 request ID、策略版本、资源类型、动作、reason 和耗时即可,主体与资源标识按数据分级做散列或遮盖。
旧的无 /v1/ Go 包在 OPA 1.x 生命周期中属于弃用路径,不等于立即删除。迁移时先换 import、编译和测试,再升级运行时;不要在同一个变更中同时重写策略语义、SDK 适配和失败策略。
Wasm 适合固定入口的本地执行
把决策入口编译进 bundle:
opa build \
-t wasm \
-e orders/authz/decision \
-o dist/orders-authz.tar.gz \
policy/orders/authz.rego
tar -tf dist/orders-authz.tar.gz产物应包含 Wasm 模块、manifest 与策略相关文件。entrypoint 使用斜杠路径,不是 REST URL。宿主按 OPA Wasm 文档加载模块、初始化 heap、设置 data 和 input、执行 entrypoint 并读取 JSON 结果。
Wasm 不是“在浏览器里启动 OPA server”。它没有 HTTP、bundle 拉取、状态上报或决策日志能力;这些都由宿主补齐。部分 built-ins 不能原生编译,需要宿主实现,升级前必须检查目标策略实际使用的 built-ins。对相同 fixture,应将 CLI 与 Wasm 结果做 golden comparison,尤其覆盖 undefined、类型错误和非 ASCII 数据,防止宿主适配层改变语义。
接进订单项目需要一层稳定适配器
业务代码不应散落 OPA URL 与 Rego path。建立 AuthorizationClient,输入使用有版本的 DTO,输出使用稳定枚举:
Decision = ALLOW | DENY | UNAVAILABLE | INVALID
reason = owner | support_ticket | default_deny | policy_error处理顺序是:从认证中间件取可信主体;从业务查询取资源 owner 与状态;构造最小 input;设置短于业务 deadline 的求值超时;严格解析结果;记录脱敏证据;只有 ALLOW 执行动作。DENY 返回业务拒绝,INVALID 表示契约或策略缺陷,UNAVAILABLE 进入事先选择的失败策略。
项目仓库可以这样组织:
services/orders/
internal/authz/client.go
internal/authz/contract.go
policy/orders/authz.rego
policy/orders/authz_test.rego
policy/data/roles.json
policy/fixtures/*.json
scripts/test-policy.shCI 先运行 opa fmt --fail、opa check --strict 和 opa test,再运行应用适配器测试。适配器测试要模拟 HTTP 200 无 result、非 JSON、超时、500、错误类型与 allow=false;否则策略单测再完整,也覆盖不到最危险的执行断点。
发布新策略时先记录不可变版本,在影子路径对同一批脱敏 input 同时求值新旧策略,只记录差异而不改变业务动作。对 allow→deny 与 deny→allow 分别评审;前者可能中断业务,后者可能扩大权限。达到约定样本量、错误率与延迟门槛后再分批切换,保留快速回读旧版本的能力。
排障按输入、版本、求值、执行四层推进
200 但没有 result
先用同一 body 查询确切 decision path,再执行 opa eval。常见原因是 package/path 拼错、规则未定义或请求体没有 input 包装。修复后必须同时看到 result 存在、类型为对象、allow 为布尔值,并确认 PEP 最终动作一致。
本地与共享环境结果不同
比较 OPA 精确版本、Rego 兼容参数、加载的 policy/data revision、完整 decision path 和规范化后的脱敏 input。不要只比较 Git 提交。将现场 input 保存为去敏 fixture,在两边用相同二进制与文件重放,逐项移除 data 差异。
规则突然 undefined
运行 opa check --strict 与 opa eval --strict-builtin-errors。Rego 某些 built-in 错误在默认求值中可能使表达式不成立,最终表现为 undefined;严格模式有助于把类型错误变成显式失败。PEP 仍要把 undefined 当拒绝,不能依赖调试参数保护生产。
CPU 与延迟上升
使用 opa eval --metrics、profiler 或 SDK metrics 找到高成本表达式,观察 input/data 大小、数组嵌套遍历、集合构造、字符串处理和外部 built-in。先减少重复扫描、预先索引 data、缩小 input,再评估缓存与运行形态。只增加副本可能把同一份大 data 复制更多次,并放大更新成本。
新策略加载后行为仍旧
区分下载、解析、编译、激活与业务调用。共享服务或 sidecar 的实例可能仍使用不同版本;健康检查成功也不能证明目标版本已激活。逐实例查询版本证据并重放同一 fixture,确认负载均衡后的每个实例给出一致结果,再恢复流量。
性能优化先改变算法与数据布局
OPA 通常把 policy 和 data 保存在内存中。性能预算至少包括 PEP 序列化、网络、队列、求值、日志遮盖和响应解析。基准样本要覆盖小/大 input、命中/拒绝、冷/热缓存、正常/峰值并发以及策略更新期间,报告 P50、P95、P99、吞吐、CPU、RSS 和错误率。
Rego 优化从减少搜索空间开始:尽早用等值条件约束对象;把频繁按 ID 查找的数组改成对象索引;避免在热路径构造巨大中间集合;把稳定的派生结果预计算进 data。使用 partial evaluation 或 Wasm 前先建立语义对比,优化后仍运行全部正反 fixture。
缓存授权结果时,key 至少包含主体、动作、资源、影响决策的上下文和策略/data 版本。只用 user + resource 会让不同动作串权;TTL 会延迟撤权,负缓存会延迟新授权。高风险写操作通常不适合长 TTL,撤权事件需要主动失效或版本切换,不能只等自然过期。
权限与敏感数据从监听端口开始
本机 CLI 只读取受审查目录;CI 使用只读 checkout 和固定二进制。HTTP 服务将决策读取与 policy/data 写入分离,业务身份只能访问必要 decision path。管理 API 放在独立网络入口,使用 TLS 与客户端身份,并以 system.authz 进一步限制方法和路径。OPA 自身的授权策略也要有 break-glass 与回退方案,避免错误规则把修复入口一起锁死。
策略包可能泄露角色、产品开关和内部对象关系,bundle 传输要加密,签名用于验证来源和完整性。签名不提供机密性,也不阻止合法旧包被重放;版本回退保护与审批要另行设计。input 与 result 进入决策日志前先遮盖 token、Cookie、密码、客户字段与可枚举资源信息,日志丢弃规则也要测试,避免拒绝事件被静默删除。
不要把生产真实请求直接放进公开 fixture。样例统一使用 alice、order-100 和 example.com;事故重放先去标识化,并限制访问、保留时间与删除责任。
HA 取舍由失败语义决定
进程内 SDK 与 Wasm 没有网络单点,策略却与应用一同升级、占用内存并共享崩溃边界。sidecar 把网络缩到 loopback,可在外部中断时继续使用最近策略,但每个实例都要加载和观测。共享 OPA 服务便于集中大 data 与弹性容量,却要求多副本、负载均衡、限流、隔离和客户端重试约束。
HA 设计必须回答四个问题:实例失联时使用旧策略多久;撤权多久必须生效;超时是拒绝、降级还是暂停;多副本版本不一致时由谁阻止流量。对退款、密钥读取和管理操作,失败关闭通常更合理;对低风险只读且能容忍陈旧决定的功能,可以在明确时限内使用本地最近版本。选择依据是业务损失与恢复目标,不是统一套用一个 fail-open 开关。
容量估算用峰值授权 QPS 乘以每个业务请求的决策次数,再加入重试与发布影子流量。共享服务要压测单实例上限、N-1 容量和策略更新风暴;sidecar 要计算总内存副本与节点密度;SDK/Wasm 要观察应用 GC 和启动时间。多副本 Ready 之后仍要证明它们加载了同一策略版本。
清理本地实验不留下隐形服务
前台 opa run 用 Ctrl+C 停止。后台进程应先找到确切 PID 与监听端口,再正常终止,不使用模糊进程名批量删除。容器实验按名称清理:
docker stop opa-lab 2>/dev/null || true
docker rm opa-lab 2>/dev/null || true
docker image inspect openpolicyagent/opa:<opa-version> >/dev/null 2>&1 && \
echo "image retained; remove only when no other project uses it"删除 dist/orders-authz.tar.gz、临时 fixture 与测试 data 前先确认它们没有被项目引用。共享环境还要撤销实验客户端证书或 token,删除临时网络规则、日志索引和缓存,不直接删除可能由其他服务共享的策略目录或镜像。最后检查 localhost:8181 不再监听,并确认 CI 没有遗留浮动下载任务。
升级从双版本语义比较开始
升级前记录旧新 OPA 版本、启动参数、Go 包路径、Wasm runtime、使用的 built-ins 和策略/data 版本。先在新 CLI 上运行格式化、严格检查、单元测试、覆盖率与所有反向 fixture;再对代表性脱敏 input 同时执行旧新版本,分类统计 allow→deny、deny→allow、reason 变化、undefined 与错误变化。
从 Rego v0 迁移时,先升级 bundle producer 并在 manifest 中明确 Rego 版本,再升级 consumers。--v0-compatible 是过渡工具,待所有模块完成 v1 语法和语义测试后移除。Go 集成分步骤迁到 /v1/ 包,Wasm 宿主单独回归 ABI 与 built-ins;避免把语言、运行时、数据模型和 PEP 错误处理塞进一次不可回退的发布。
滚动升级共享服务时保留旧池,新池先接影子请求,再接少量真实流量;指标不仅看错误率,还看决定差异、P99、CPU、内存和超时。若出现未解释的开放差异、undefined 增长、冲突或长尾突破门槛,立即回读旧池。升级完成后删除兼容参数、旧二进制、旧容器、废弃凭证和过期策略副本,但保留受控回滚窗口内需要的不可变制品。
OPA 与 Rego 最终交付的不是一份能编译的策略,而是一条可证明的决策链:可信 input 进入明确路径,policy 与 data 版本可追踪,undefined 和错误稳定关闭,PEP 执行动作与决定一致,性能与故障有预算,权限、日志、升级和退出都有 owner。做到这些,策略从文本变成了可以长期运行的工程部件。
