当权限逻辑从代码中剥离,成为可独立演进、可审计、可测试的一等公民时,系统便需要一个”策略引擎”。OPA(Open Policy Agent)与 AWS Cedar 是当下最具代表性的两种选择,它们背后代表着两种截然不同的哲学:一个是图灵完备的通用策略语言,一个是牺牲表达力换取可判定性与形式化验证的领域语言。本文从 PDP/PEP/PIP/PAP 的经典模型出发,深入剖析两者在语言、运行时、部署、测试与治理上的差异,并给出工程落地的决策矩阵。
一、策略引擎的定位:PDP/PEP/PIP/PAP
策略引擎并非凭空而来,它的角色分工最早由 XACML(eXtensible Access Control Markup Language)标准在 2003 年固化下来。理解这四个缩写,是理解后续所有策略引擎设计的前提。
1.1 四个角色的职责
PEP(Policy Enforcement Point,策略执行点):拦截请求、构造授权查询、执行决策结果。它是业务流量的必经之路,通常表现为 API Gateway、Service Mesh 的 Sidecar、中间件、或服务内部的拦截器。PEP 不关心”为什么允许”,只关心”是否允许”。
PDP(Policy Decision Point,策略决策点):接收 PEP 的查询,加载策略与数据,计算出 allow/deny。PDP 是”思考”的地方,OPA、Cedar、AWS IAM Evaluator 都扮演这个角色。
PIP(Policy Information Point,策略信息点):PDP 在决策时需要的额外属性来源。例如”用户所在部门”、“资源的 owner”、“当前时间是否工作时间”。PIP 可以是数据库、外部 API、缓存,也可以是预加载到 PDP 的静态数据。
PAP(Policy Administration Point,策略管理点):策略的编写、评审、发布、回滚。PAP 是”治理”的地方,通常对应 Git 仓库、Bundle Server、CI/CD 流水线。
1.2 一次完整的请求流程
以”Alice 访问 GET /api/documents/123”为例,一次完整授权流程如下:
1. Client ──── HTTP Request ───→ PEP(Service / Sidecar)
2. PEP ──── Query(input) ───→ PDP(OPA / Cedar)
3. PDP ──── Fetch Attrs ───→ PIP(DB / Cache / API)
PDP ←─── Attributes ──── PIP
4. PDP ──── Decision ───→ PEP
5. PEP ──── Forward ───→ Upstream Service
(or) ──── 403 Denied ───→ Client
关键点:
- PEP 到 PDP 的查询是一次 RPC(或库函数调用),必须低延迟,通常要求 P99 小于 10ms。
- PDP 加载的策略与数据来自 PAP,通过 Bundle 或推送方式同步,更新是异步的。
- PIP 访问是 PDP 最容易成为瓶颈的地方,优化手段一般是把常用属性”内化”到 PDP 的 data 中。
1.3 为什么不把权限逻辑写进业务代码
很多团队早期都会把”if user.role == admin”这类判断散落在各处。问题在两年后才会显现:
- 权限逻辑横跨几十个微服务,每次合规审计都要翻遍整个代码库。
- 改一条规则(例如”外包员工不能删除生产数据”)要改十几个 PR。
- 测试覆盖基本是空白,因为权限判断散落在业务流里。
- 安全团队看不到全貌,只能靠运行时拦截。
把策略从代码里剥离,就是为了让”谁能做什么”变成可以独立演进、独立评审、独立测试的对象。这是策略引擎存在的根本理由。
1.4 ABAC、RBAC、ReBAC 都可以落在策略引擎上
策略引擎本身是”模型无关”的。你可以在 OPA 里写纯 RBAC 规则,也可以写 ABAC(属性驱动),甚至实现 ReBAC 风格的关系判断(虽然 Zanzibar 风格的倒排索引用 OPA 表达会很别扭,见上一篇《Zanzibar 风格权限系统》)。Cedar 同样支持这三类模型,但它对关系路径的表达能力有严格限制。
二、OPA 与 Rego:设计哲学与核心机制
OPA 由 Styra 创立,2018 年进入 CNCF,2021 年毕业。它的定位从一开始就不是”一个授权库”,而是”一个通用策略引擎”:凡是需要”在某个上下文下判断是否允许”的场景,OPA 都可以介入。
2.1 OPA 的三类典型用途
- API 授权:每个 API 请求前查询 PDP,判断当前用户能否访问。
- Kubernetes 准入控制:通过 Gatekeeper 项目,OPA 作为 Admission Webhook 拦截所有 kubectl apply,拒绝违反策略的资源(例如”禁止 privileged 容器”)。
- 数据过滤:通过 Partial Evaluation,把”用户能看到的行”转换成 SQL WHERE 子句。
- 其他:CI/CD 策略、Terraform 策略(Conftest)、服务网格授权(Istio)、容器镜像签名验证。
一个策略引擎打通这么多场景,意味着团队只需要学一门策略语言,就能覆盖绝大多数控制面需求。
2.2 Rego:Datalog 的血脉
Rego 受 Datalog 启发,是一种声明式、基于合一(unification)的语言。理解 Rego 的关键,是放弃命令式的”一步一步执行”思维,接受”描述真值约束”的思维。
一个最简单的 Rego 策略:
package authz
import future.keywords.if
import future.keywords.in
default allow := false
allow if {
input.method == "GET"
input.path[0] == "api"
input.path[1] == "public"
}
allow if {
some role in data.user_roles[input.user]
some permission in data.role_permissions[role]
permission.resource == input.resource
permission.action == input.action
}
这里有几个必须理解的概念:
default allow := false:在没有任何规则成立时,allow
默认是 false。没有 default 的变量在未匹配时是”undefined”,而
undefined 不等于 false(这是 Rego 最大的坑之一)。
多个 allow if {...}
块:它们是”或”的关系。只要任意一个块的条件全部成立,allow
就为 true。
some x in collection:存在量词,只要集合里存在一个
x 使后续条件成立,这条规则就成立。
合一语义:input.method == "GET"
在 Rego 里不是”赋值”也不是单纯的”比较”,而是”合一”:如果
input.method 不存在,这个表达式不是 error,而是
undefined。
2.3 输入(input)与数据(data)的二分
Rego 的运行时有两个命名空间:
input:每次查询传入的请求上下文(用户、资源、方法、headers 等)。data:预加载到 PDP 内存的全局数据(用户角色表、资源拥有者、组织架构等)。
这个二分设计是 OPA 性能的关键。data 常驻内存并被索引,查询时只需要与 input 做匹配,不需要每次都去数据库拉。
data 的加载方式有三种:
- Inline(内联):策略文件旁边直接放 data.json,随 bundle 一起发。
- Bundle API:OPA 周期性从 Bundle Server 拉取 tar.gz 包。
- Discovery:OPA 启动时拉取一份配置,再据此拉取真正的 bundle。
2.4 Bundle 结构
一个典型的 OPA bundle 长这样:
bundle.tar.gz
├── .manifest
├── authz/
│ ├── policy.rego
│ └── data.json
├── k8s/
│ ├── admission.rego
│ └── data.json
└── common/
└── helpers.rego
.manifest 文件记录 revision、root 等信息:
{
"revision": "2026-04-21-abc123",
"roots": ["authz", "k8s", "common"]
}Bundle 的原子性很重要:OPA 在内存中构建完新 bundle 之后才切换,切换期间不会有半新半旧的状态。
2.5 OPA 的 HTTP API
OPA 启动后默认监听 8181 端口,查询策略可以直接用 curl:
curl -X POST http://localhost:8181/v1/data/authz/allow \
-H 'Content-Type: application/json' \
-d '{
"input": {
"user": "alice",
"method": "GET",
"path": ["api", "documents", "123"],
"resource": "document:123",
"action": "read"
}
}'返回:
{
"result": true,
"decision_id": "fbcd3b7f-3bbf-4e9c-9a21-8f5a2f01234c"
}在 Go 里使用 SDK 嵌入调用:
package main
import (
"context"
"fmt"
"github.com/open-policy-agent/opa/rego"
)
func main() {
ctx := context.Background()
r := rego.New(
rego.Query("data.authz.allow"),
rego.Load([]string{"./policies"}, nil),
)
query, err := r.PrepareForEval(ctx)
if err != nil {
panic(err)
}
input := map[string]interface{}{
"user": "alice",
"method": "GET",
"path": []string{"api", "documents", "123"},
"resource": "document:123",
"action": "read",
}
rs, err := query.Eval(ctx, rego.EvalInput(input))
if err != nil {
panic(err)
}
if len(rs) > 0 && rs[0].Expressions[0].Value == true {
fmt.Println("allowed")
} else {
fmt.Println("denied")
}
}PrepareForEval 会把策略编译成内部中间表示,后续 Eval 只做求值,单次调用通常在几十微秒到几毫秒。
2.6 Partial Evaluation:数据过滤的杀手锏
OPA 有一个独特能力:部分求值。给定一个查询和一部分 input(例如 user 已知,但 resource 未知),OPA 可以把策略”化简”成只依赖未知变量的残余表达式,再把这个表达式翻译成 SQL WHERE 子句。
场景:列表查询。用户 alice 请求 GET /documents,我们希望只返回 alice 能看到的文档。常规做法有两种:
- 拉全表,逐条问 OPA—— O(n) 次查询,慢。
- 把 OPA 策略翻译成 SQL —— 只查一次,快。
第二种就是 Partial Evaluation。OPA 提供了 compile API:
curl -X POST http://localhost:8181/v1/compile \
-H 'Content-Type: application/json' \
-d '{
"query": "data.authz.allow == true",
"input": {"user": "alice", "action": "read"},
"unknowns": ["input.resource"]
}'返回的 AST 经过转换后可以生成:
SELECT * FROM documents
WHERE owner = 'alice' OR classification = 'public';这个能力对”N 条数据级别权限”的场景几乎是降维打击。但要小心:不是所有 Rego 策略都能完全部分求值,含有 http.send、time.now 等副作用函数的规则会无法化简。
三、Rego 的坑:全表扫、数据加载与 Bundle
Rego 写法优雅,但生产环境里有几类坑必须提前知道。
3.1 全表扫描陷阱
最常见的反模式:
allow if {
some user in data.users
user.id == input.user_id
user.role == "admin"
}
这段代码看起来没问题,但 OPA 会遍历 data.users 里的每一条记录来做合一。如果 users 有十万条,单次查询就是十万次比较,P99 会飙到几百毫秒。
正确写法是用键索引:
allow if {
user := data.users[input.user_id]
user.role == "admin"
}
这里 data.users 是 object(map),用 key 直接查是 O(1)。
OPA 1.0 之后引入了自动索引优化(rule indexing),对”字面值等于”这类条件会自动建索引。但依赖引用、嵌套结构的索引仍然有限,写策略时养成”优先用 map 查找”的习惯是必要的。
3.2 undefined 不是 false
Go/Java 背景的同学最容易踩的坑:
allow if {
input.user.role == "admin"
}
如果 input.user 不存在,这条规则的结果是 undefined,不是 false。因为有 default allow := false,最终返回 false,感觉没问题。但如果你写的是:
deny if {
input.user.role != "admin"
}
意图是”非 admin 就拒绝”。但如果 input.user.role
不存在,!= "admin" 结果是 undefined,deny
不会触发。你以为拒绝了,实际放行了。
正确的防御性写法:
deny if {
not is_admin
}
is_admin if {
input.user.role == "admin"
}
用 not 对已定义的规则取反,语义是清晰的。
3.3 Bundle 更新的传播延迟
OPA 的 bundle 默认每 60 秒拉一次。这意味着:
- 吊销用户权限后,最长 60 秒内还能通过旧策略。
- 策略发布后,几百个 PDP 实例不是同时切换的。
这是 OPA 的最终一致性模型,和 Zanzibar 的 Zookie 机制完全不同。如果业务需要”吊销立即生效”,有两种选择:
- 把用户状态放到独立的快速通道(例如每请求都查一次 Redis 黑名单),策略里只判断”是否在黑名单”。
- 用 OPA 的 push 模型:Bundle Server 主动推送更新(需要自研或使用 Styra DAS)。
3.4 Bundle 大小与冷启动
见过一个真实案例:某公司把 500MB 的用户数据塞进 bundle,PDP 启动时拉包、解压、构建索引花了 40 秒。Kubernetes 滚动更新时,Pod 就绪探针要等 40 秒,批量扩容阶段整个集群可用性抖动。
经验法则:
- 策略文件应该小(单个 rego 文件几 KB)。
- data 部分如果超过几 MB,考虑拆分:热数据进 bundle,冷数据通过 PIP 按需查询。
- 超大租户(百万级主体)不适合直接塞 data,应该用外部 attribute API。
3.5 Rego 学习曲线
团队落地 OPA 的第一个月,通常都会经历”写出来的策略 90% 是对的,剩下 10% 是诡异的 undefined”这种阶段。建议:
- 新人先写 unit test,用 opa test
跑,看到失败先不要改策略,先查
opa eval --explain=full。 - 团队共享一个”Rego 陷阱清单”,把踩过的坑记下来。
- Code review 时重点看:有没有 default?有没有 iterate 大集合?有没有对 undefined 做 not 操作?
四、Cedar:AWS 的形式化验证方案
AWS 在 2023 年 re:Invent 开源 Cedar,同期推出 Amazon Verified Permissions 服务。Cedar 的目标非常明确:为 SaaS 应用提供一个”简单、安全、可验证”的授权语言。
4.1 设计哲学:牺牲表达力换可判定性
Cedar 的核心约束是:策略求值永远终止,且求值结果可以被形式化验证。这意味着 Cedar 不是图灵完备的——它没有递归、没有无限循环、没有外部调用。
这个约束带来的收益是巨大的:
- 可以静态分析两条策略是否有冲突。
- 可以证明某条策略是否”overpermissive”(授权范围超出预期)。
- 可以在不真跑策略的情况下,判断”能否在某些输入下达到 allow”。
这些在 Rego 里是不可能做到的,因为 Rego 图灵完备,等价于停机问题。
4.2 Cedar 策略示例
最基本的形式:
permit(
principal == User::"alice",
action in [Action::"view", Action::"edit"],
resource == Document::"quarterly-report"
);
含义:允许 Alice 对 quarterly-report 文档执行 view 或 edit。
带条件的形式:
permit(
principal in Team::"engineering",
action == Action::"view",
resource is Document
) when {
resource.classification == "internal"
};
含义:engineering 团队的成员可以查看任何 Document,但仅当 classification 为 internal 时。
拒绝策略(forbid)优先于 permit:
forbid(
principal,
action,
resource is Document
) when {
resource.classification == "confidential"
&& !(principal in Team::"security")
};
这里 principal 不写具体的 == 表示”任何
principal”。只要资源是 confidential 且用户不在 security
团队,无论有多少 permit,最终都是 deny。
4.3 实体模型(Entity Model)
Cedar 的世界由 Entity 构成,每个 Entity 有:
- 类型(User、Document、Team 等)
- 唯一 ID(
User::"alice") - 属性(department、email、classification 等)
- 父实体(parents,表达归属关系)
一个典型的实体 JSON:
{
"uid": { "type": "User", "id": "alice" },
"attrs": {
"department": "engineering",
"level": 5
},
"parents": [
{ "type": "Team", "id": "engineering" },
{ "type": "Team", "id": "reviewers" }
]
}principal in Team::"engineering" 就是检查
principal 的 parents 链路上是否包含这个 Team。这和 OPA 里用
data 存嵌套关系的思路完全不同——Cedar
把”关系”做成了一等公民。
4.4 Schema 与验证器
Cedar 支持可选的 Schema 定义:
{
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"department": { "type": "String" },
"level": { "type": "Long" }
}
},
"memberOfTypes": ["Team"]
},
"Document": {
"shape": {
"type": "Record",
"attributes": {
"classification": { "type": "String" }
}
}
}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Document"]
}
}
}
}有了 Schema,Cedar Validator 可以在策略编写时就发现:
- 引用不存在的属性(
principal.departmnt,拼错了)。 - 类型错误(
resource.classification == 1,但 schema 里是 String)。 - Action 与 Principal/Resource 类型不匹配(User 对 Team 执行 view)。
这是 Cedar 相对 Rego 的重要优势:Rego 是动态类型的,这类错误要到运行时才能发现。
4.5 形式化验证
Cedar 背后有一套基于 Lean 4 的形式化证明,可以证明:
- 策略求值的可终止性。
- 等价性:两组策略在任意输入下结果相同。
- 包含性:策略 A 的授权集合是否是策略 B 的子集。
在 AWS Verified Permissions 中,这些能力被封装成 API,你可以问”我这次策略更新是否扩大了授权范围”。这对金融、医疗等合规敏感场景是极有吸引力的。
4.6 Cedar 的 Rust SDK 调用
use cedar_policy::{Authorizer, Context, Entities, PolicySet, Request, EntityUid};
fn main() {
let policies: PolicySet = r#"
permit(
principal == User::"alice",
action == Action::"view",
resource == Document::"report"
);
"#.parse().unwrap();
let principal: EntityUid = "User::\"alice\"".parse().unwrap();
let action: EntityUid = "Action::\"view\"".parse().unwrap();
let resource: EntityUid = "Document::\"report\"".parse().unwrap();
let request = Request::new(
Some(principal), Some(action), Some(resource),
Context::empty(), None
).unwrap();
let entities = Entities::empty();
let authorizer = Authorizer::new();
let response = authorizer.is_authorized(&request, &policies, &entities);
println!("Decision: {:?}", response.decision());
}五、Rego vs Cedar 对比
| 维度 | OPA / Rego | AWS Cedar |
|---|---|---|
| 语言范式 | Datalog 风格,声明式,合一语义 | 领域特定,permit/forbid 结构化语法 |
| 图灵完备 | 是 | 否(刻意不是) |
| 可判定性 | 无(可能无限循环) | 有形式化证明 |
| 形式化验证 | 无内建,需外部工具 | 有(Lean 证明,Validator 内建) |
| 类型系统 | 动态,无 schema | 静态,支持 Schema |
| 表达力 | 极高,可做递归、函数、复杂聚合 | 有限,为可验证性做了剪裁 |
| 性能 | 查询亚毫秒到几毫秒,支持 rule indexing | 查询亚毫秒,策略编译快 |
| 部分求值 | 支持(Partial Evaluation) | 不支持 |
| 数据模型 | input + data 二分,data 是任意 JSON | Entity Model,关系通过 parents 表达 |
| 生态 | CNCF,集成广泛(K8s、Istio、Envoy、Terraform) | AWS 生态为主,开源库有 Rust/Java/Go/TS |
| 部署 | Sidecar / Daemon / Library,自运维或 Styra DAS | 自建 + AVP 托管服务 |
| 学习曲线 | 陡,合一/undefined 是门槛 | 平缓,像读英语 |
| 适用场景 | 通用策略(K8s、CI、微服务授权、数据过滤) | 应用级授权,尤其 SaaS |
| 多语言 SDK | Go 原生,其他语言通过 REST 或 WASM | Rust 原生,Java/Go/TypeScript/Python |
| 审计与合规 | 需自行建审计通道 | AVP 内建审计、合规证明 |
一句话总结:Rego 是瑞士军刀,Cedar 是手术刀。前者什么都能切,后者在应用授权这个具体场景下更安全、更易治理。
六、部署模式
策略引擎的部署方式,直接决定了它的延迟、可用性、运维成本。
6.1 Sidecar 模式
最常见的 Kubernetes 部署:每个应用 Pod 里塞一个 OPA 容器,应用通过 localhost:8181 访问。
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
template:
spec:
containers:
- name: api
image: myapp/api:1.0
env:
- name: OPA_URL
value: "http://localhost:8181"
- name: opa
image: openpolicyagent/opa:latest
args:
- "run"
- "--server"
- "--config-file=/config/opa-config.yaml"
volumeMounts:
- name: opa-config
mountPath: /config
ports:
- containerPort: 8181
volumes:
- name: opa-config
configMap:
name: opa-configopa-config.yaml 告诉 OPA 从哪拉 bundle:
services:
bundle-server:
url: https://bundles.example.com
credentials:
bearer:
token_path: /var/run/secrets/bundle-token
bundles:
authz:
service: bundle-server
resource: /bundles/authz.tar.gz
polling:
min_delay_seconds: 30
max_delay_seconds: 60
decision_logs:
service: bundle-server
reporting:
min_delay_seconds: 5
max_delay_seconds: 30优点:localhost 调用几乎零网络延迟;每个 Pod 独立加载策略,故障隔离。
缺点:资源开销大(每个 Pod 多一份 OPA 进程和 bundle 内存);大规模时 bundle 服务器承担 N 倍的拉取流量。
6.2 Daemon 模式
OPA 作为独立的集群内服务,多个应用共享:
apiVersion: apps/v1
kind: Deployment
metadata:
name: opa
spec:
replicas: 5
template:
spec:
containers:
- name: opa
image: openpolicyagent/opa:latest
args: ["run", "--server", "--config-file=/config/opa.yaml"]
---
apiVersion: v1
kind: Service
metadata:
name: opa
spec:
selector:
app: opa
ports:
- port: 8181优点:资源集中,bundle 加载一次多处用;可以独立扩缩容。
缺点:多一跳网络(通常 1-3ms),单点故障面扩大,所有应用都依赖这个服务。
6.3 Library 模式
把 OPA 作为库嵌入应用进程,例如 Go 服务直接 import
github.com/open-policy-agent/opa/rego。
// 启动时加载策略
query, _ := rego.New(
rego.Query("data.authz.allow"),
rego.Load([]string{"/etc/policies"}, nil),
).PrepareForEval(ctx)
// 每次请求求值
rs, _ := query.Eval(ctx, rego.EvalInput(input))优点:零网络延迟;没有额外进程要运维。
缺点:策略更新要重启(或自己实现热加载);不适合非 Go 语言;与业务代码耦合紧。
6.4 云托管
- Amazon Verified Permissions:托管 Cedar,免运维,与 Cognito、API Gateway 集成。
- Styra DAS:托管 OPA,提供策略可视化、影响分析、审批流。
- Permit.io / Oso Cloud:SaaS 形态的策略平台。
托管方案的好处是省心,但要接受”所有授权查询都要过第三方”这个事实。金融、医疗、政府场景通常不选托管。
6.5 OPA Gatekeeper:Kubernetes 准入控制
Gatekeeper 是 OPA 在 Kubernetes 生态里的明星用例。它通过 ValidatingAdmissionWebhook 拦截所有 kubectl 请求。
定义约束模板(ConstraintTemplate):
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels
provided := input.review.object.metadata.labels
missing := required[_]
not provided[missing]
msg := sprintf("missing required label: %v", [missing])
}再基于模板创建约束:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-must-have-owner
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["owner"]从此,任何没有 owner label 的 namespace 都无法创建。
七、策略测试与治理
策略是代码,就应该有 CI、有 review、有版本、有回滚。否则”把权限逻辑从代码剥离”只是从一个泥潭换到另一个泥潭。
7.1 OPA 的测试框架
Rego 自带测试支持,文件名以 _test.rego
结尾:
package authz_test
import data.authz
test_allow_public_api if {
authz.allow with input as {
"method": "GET",
"path": ["api", "public"],
"user": "anyone"
}
}
test_deny_non_public_anonymous if {
not authz.allow with input as {
"method": "GET",
"path": ["api", "private"]
}
}
test_allow_admin if {
authz.allow with input as {
"user": "alice",
"resource": "document:123",
"action": "delete"
} with data.user_roles as {"alice": ["admin"]}
with data.role_permissions as {
"admin": [{"resource": "document:123", "action": "delete"}]
}
}
运行测试:
$ opa test -v .
data.authz_test.test_allow_public_api: PASS (512µs)
data.authz_test.test_deny_non_public_anonymous: PASS (231µs)
data.authz_test.test_allow_admin: PASS (892µs)
--------------------------------------------------------------------------------
PASS: 3/3覆盖率:
$ opa test --coverage --format=json . | jq '.files["authz/policy.rego"].coverage'
94.57.2 Cedar 的测试
Cedar 的测试通常用 Rust/Java/TS SDK 写单元测试,结构化调用 Authorizer:
#[test]
fn test_alice_can_view_report() {
let policies = load_policies("policies/").unwrap();
let entities = load_entities("entities.json").unwrap();
let request = Request::new(
Some("User::\"alice\"".parse().unwrap()),
Some("Action::\"view\"".parse().unwrap()),
Some("Document::\"report\"".parse().unwrap()),
Context::empty(), None
).unwrap();
let auth = Authorizer::new();
let resp = auth.is_authorized(&request, &policies, &entities);
assert_eq!(resp.decision(), Decision::Allow);
}7.3 CI/CD 流水线
一个合格的策略 CI 流水线至少包含:
name: policy-ci
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: setup opa
run: |
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod +x opa
- name: fmt check
run: ./opa fmt --diff policies/
- name: lint
run: ./opa check --strict policies/
- name: test
run: ./opa test --coverage --threshold=90 policies/
- name: build bundle
run: ./opa build -b policies/ -o bundle.tar.gz
- name: sign bundle
run: cosign sign-blob bundle.tar.gz
- name: upload
if: github.ref == 'refs/heads/main'
run: aws s3 cp bundle.tar.gz s3://policies/authz.tar.gz关键点:
opa fmt保证风格一致,diff 模式在 CI 里失败。opa check --strict捕捉弃用语法、未使用变量。- 覆盖率阈值强制执行。
- Bundle 签名,PDP 启动时验签,防止供应链攻击。
7.4 Decision Log 与审计
OPA 的 decision log 记录每一次授权决策:
{
"decision_id": "fbcd3b7f-3bbf-4e9c-9a21-8f5a2f01234c",
"timestamp": "2026-04-21T10:23:45.123Z",
"path": "authz/allow",
"input": {
"user": "alice",
"method": "GET",
"path": ["api", "documents", "123"]
},
"result": true,
"bundles": {
"authz": { "revision": "2026-04-21-abc123" }
}
}这份日志的价值巨大:
- 合规审计:六个月后被问到”为什么 bob 在 3 月 14 日能删除那份合同”,可以精确定位到当时的策略版本、输入上下文、决策结果。
- 影响分析:发布新策略前,拿历史 decision log replay 一遍,看结果是否一致。
- 异常检测:用户突然拿到大量 allow 决策,可能是权限放大攻击。
7.5 影响分析(Policy Diff)
发布新策略前,最好回答这个问题:这次改动会让哪些用户的哪些决策从 deny 变 allow,或反之。
一种做法:把过去 24 小时的 decision
log(input)灌进新旧两个 PDP,diff 结果。Cedar 在 AVP
里直接提供 PolicyAnalysis
API,可以静态分析等价性,不需要真实数据。
7.6 变更评审
策略改动要当代码审:
- PR 必须有至少两个 reviewer,其中一个来自安全团队。
- CI 通过才能合并。
- 合并后自动生成 bundle,推送到 staging,观察 24 小时的 decision log,再升级到生产。
- 生产变更有明确的回滚路径(bundle 版本回退)。
八、工程坑点
8.1 Decision Log 体量
如果每个请求都落日志,大流量服务每天会产生几 TB。落地建议:
- 采样:默认 1% 采样,只记录 deny 决策的 100% 采样。
- 异步批量:OPA 原生支持 decision log 缓冲和批量发送。
- 分层存储:热数据 7 天在 Kafka/Loki,温数据 90 天 S3,冷数据 7 年归档。
- 脱敏:日志可能含敏感 input(用户邮箱、资源内容),落盘前做字段级脱敏。
8.2 PDP 冷启动
几个应对方案:
- 把大 data 拆成多个 bundle,小 bundle 先加载先提供服务,大 bundle 后加载。
- 用就绪探针控制:bundle 加载完才 ready,而不是进程起来就 ready。
- Bundle Server 靠近 PDP(同 region 的 S3),减少拉取延迟。
- 集群滚动更新时,surge 控制:每次只替换 1 个 Pod,不是 maxSurge=100%。
8.3 策略-数据耦合
反模式:把业务数据塞进策略。
# 不要这样
allow if {
input.user == "alice"
input.path[0] == "admin"
}
allow if {
input.user == "bob"
input.path[0] == "reports"
}
每加一个用户就改策略。正确的做法是把用户-权限映射放到 data:
allow if {
data.user_permissions[input.user][_] == sprintf("%s:%s", [input.resource_type, input.action])
}
data 由业务系统生成,策略保持稳定。策略文件里只有”规则的形状”,没有”具体的人”。
8.4 多租户污染
另一个常见错误:把所有租户的数据塞到同一个 data 里:
{
"tenants": {
"acme": { "users": [...] },
"globex": { "users": [...] }
}
}这会导致:
- 策略每次都要先
input.tenant索引一次,容易漏写导致跨租户越权。 - Bundle 膨胀:一个小租户的查询也要加载所有租户的数据。
- 隔离性差:一个租户的数据错误会影响所有租户。
更好的做法:每个租户一个独立 bundle,或用 OPA 的多 bundle 能力把 data root 按 tenant 分离。Cedar 的 PolicyStore 天然支持按租户分离。
8.5 策略里做 HTTP 调用
Rego 支持 http.send,可以在策略求值时调外部
API。这看起来很方便,但后果严重:
- 每次查询增加外部依赖的延迟。
- 外部服务不可用时策略求值失败。
- Partial Evaluation 无法化简。
原则:PDP 应该是”纯函数”,输入是 input 和 data,输出是 decision,中间不做 I/O。需要的属性预先拉到 data 里,或让 PEP 负责注入到 input 里。
8.6 隐式 deny 与显式 forbid
Rego 没有 forbid,只有 allow。但实际场景里经常需要”紧急吊销某人”,这时通常在 allow 规则里加 not 条件:
default allow := false
allow if {
has_role
not is_revoked
}
is_revoked if {
input.user in data.revocations
}
Cedar 的 forbid 在这方面更直接:
forbid(
principal,
action,
resource
) when {
principal in Group::"revoked"
};
forbid 永远压过 permit。设计上更符合”拒绝优于允许”的安全直觉。
8.7 Bundle 签名与供应链
Bundle 是代码,代码就可能被篡改。2024 年有过真实案例:某公司 CI 被攻破,攻击者推送了一个”总是 allow”的恶意 bundle,生产环境全线放行 30 分钟。
防御:
- Bundle 打包时签名(cosign、Sigstore)。
- OPA 配置
verification字段,加载时验签。 - Bundle Server 的写权限最小化,只允许 CI 的特定 service account。
bundles:
authz:
service: bundle-server
resource: /bundles/authz.tar.gz
signing:
keyid: authz-publisher-key
scope: read九、选型建议
没有银弹,只有适合场景的选择。下面是一个实操的决策矩阵:
| 如果你的场景是… | 推荐 |
|---|---|
| Kubernetes 准入控制 | OPA Gatekeeper |
| 微服务 API 授权,多语言栈 | OPA(daemon 或 sidecar) |
| 数据库行级权限(WHERE 子句生成) | OPA(Partial Evaluation) |
| SaaS 应用授权,多租户 | Cedar(AVP 或自建) |
| 高合规要求(金融、医疗),需形式化证明 | Cedar |
| Terraform / IaC 策略 | OPA(Conftest) |
| 单一 Go 服务,策略简单 | OPA Library 或自写 |
| 已深度绑定 AWS | Cedar + AVP |
| 需要复杂的关系图遍历(深层嵌套权限) | Zanzibar 类方案(见上一篇) |
| 团队 Rust 为主 | Cedar(原生 Rust) |
| 团队无策略经验,希望快速上手 | Cedar(语法简单) |
| 需要在策略里做复杂聚合、递归 | OPA(Rego 图灵完备) |
几个常见误区:
- “先写 OPA,以后再换”:换策略引擎的成本远比想象中高,语言差异、数据模型差异、测试资产、团队技能都要重建。选型阶段认真评估。
- “Cedar 是 AWS 的,我不用 AWS 就不能用”:Cedar 是开源的,Rust/Java/Go/TS SDK 都可以独立部署,不依赖 AVP。
- “策略引擎是银弹”:它只负责”是否允许”,不负责”授权数据怎么建模”、“权限怎么发放”、“审计怎么呈现”。策略引擎是整个身份治理体系的一个组件,不是全部。
落地路线图(针对一个中型团队的建议):
- 第 1 个月:选定引擎,搭最小 PDP(单 sidecar 或 daemon),迁移一个低风险服务的授权逻辑。
- 第 2 个月:建 CI 流水线,落地 opa test / cedar test,规范策略 PR 流程。
- 第 3 个月:接入 decision log,建审计看板。
- 第 4-6 个月:迁移核心服务,引入 Bundle 签名,做影响分析工具。
- 持续:策略治理委员会、定期策略 review、废弃无效策略。
十、参考资料
- XACML 3.0 Specification — OASIS
- Open Policy Agent — https://www.openpolicyagent.org/
- Rego Language Reference — https://www.openpolicyagent.org/docs/latest/policy-language/
- OPA Performance Tips — https://www.openpolicyagent.org/docs/latest/policy-performance/
- AWS Cedar — https://www.cedarpolicy.com/
- Cedar Language Guide — https://docs.cedarpolicy.com/
- Amazon Verified Permissions — https://aws.amazon.com/verified-permissions/
- Cedar: A New Language for Authorization — AWS 论文, 2023
- OPA Gatekeeper — https://open-policy-agent.github.io/gatekeeper/
- Styra DAS 文档 — https://docs.styra.com/
- 本系列上一篇:Zanzibar 风格权限系统
- 相关:授权架构总览
上一篇:Zanzibar 风格权限系统
下一篇:B2B SaaS 多租户权限设计
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【身份与访问控制工程】OAuth 2.1 与 PKCE:现代授权主路径
从一次 SPA 安全事故出发,系统梳理 OAuth 2.1 相对 2.0 的收敛动作、PKCE 的密码学原理、授权码流程的完整参数细节,以及 DPoP、PAR、JAR、RAR 等现代扩展与常见攻击面
【身份与访问控制工程】RBAC、ABAC、ReBAC:权限模型怎么选
从角色爆炸问题切入,深入对比 RBAC(NIST 四级模型)、ABAC(XACML)、ReBAC(Zanzibar 启发)三种权限模型的数据结构、SQL 建模、策略表达能力与工程取舍,给出混合模型实践与选型决策树。
【身份与访问控制工程】Zanzibar 风格权限系统
深入解析 Google Zanzibar 论文(USENIX ATC 2019)的核心设计:Relation Tuple、Namespace、Userset Rewrite,一致性模型(Zookies)与开源实现 SpiceDB、OpenFGA、Ory Keto 的工程对比,以及适用与不适用场景。
【身份与访问控制工程】B2B SaaS 多租户权限设计
B2B SaaS 的权限问题远比 B2C 复杂:多个企业客户、各自的内部角色体系、跨租户协作、行列级数据权限、租户自助管理。本文从隔离模型出发,给出租户内 RBAC + 租户间 ReBAC 的混合方案、超级管理员设计、行级权限实现,以及 GitHub、Slack、Notion 的权限模型速览。