土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】OPA、Cedar 与策略引擎落地

文章导航

分类入口
architecturesecurity
标签入口
#OPA#Rego#Cedar#policy-engine#PDP#authorization

目录

当权限逻辑从代码中剥离,成为可独立演进、可审计、可测试的一等公民时,系统便需要一个”策略引擎”。OPA(Open Policy Agent)与 AWS Cedar 是当下最具代表性的两种选择,它们背后代表着两种截然不同的哲学:一个是图灵完备的通用策略语言,一个是牺牲表达力换取可判定性与形式化验证的领域语言。本文从 PDP/PEP/PIP/PAP 的经典模型出发,深入剖析两者在语言、运行时、部署、测试与治理上的差异,并给出工程落地的决策矩阵。

PDP/PEP 架构

一、策略引擎的定位: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

关键点:

1.3 为什么不把权限逻辑写进业务代码

很多团队早期都会把”if user.role == admin”这类判断散落在各处。问题在两年后才会显现:

把策略从代码里剥离,就是为了让”谁能做什么”变成可以独立演进、独立评审、独立测试的对象。这是策略引擎存在的根本理由。

1.4 ABAC、RBAC、ReBAC 都可以落在策略引擎上

策略引擎本身是”模型无关”的。你可以在 OPA 里写纯 RBAC 规则,也可以写 ABAC(属性驱动),甚至实现 ReBAC 风格的关系判断(虽然 Zanzibar 风格的倒排索引用 OPA 表达会很别扭,见上一篇《Zanzibar 风格权限系统》)。Cedar 同样支持这三类模型,但它对关系路径的表达能力有严格限制。

二、OPA 与 Rego:设计哲学与核心机制

OPA 由 Styra 创立,2018 年进入 CNCF,2021 年毕业。它的定位从一开始就不是”一个授权库”,而是”一个通用策略引擎”:凡是需要”在某个上下文下判断是否允许”的场景,OPA 都可以介入。

2.1 OPA 的三类典型用途

一个策略引擎打通这么多场景,意味着团队只需要学一门策略语言,就能覆盖绝大多数控制面需求。

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 的运行时有两个命名空间:

这个二分设计是 OPA 性能的关键。data 常驻内存并被索引,查询时只需要与 input 做匹配,不需要每次都去数据库拉。

data 的加载方式有三种:

  1. Inline(内联):策略文件旁边直接放 data.json,随 bundle 一起发。
  2. Bundle API:OPA 周期性从 Bundle Server 拉取 tar.gz 包。
  3. 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 能看到的文档。常规做法有两种:

  1. 拉全表,逐条问 OPA—— O(n) 次查询,慢。
  2. 把 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 秒拉一次。这意味着:

这是 OPA 的最终一致性模型,和 Zanzibar 的 Zookie 机制完全不同。如果业务需要”吊销立即生效”,有两种选择:

  1. 把用户状态放到独立的快速通道(例如每请求都查一次 Redis 黑名单),策略里只判断”是否在黑名单”。
  2. 用 OPA 的 push 模型:Bundle Server 主动推送更新(需要自研或使用 Styra DAS)。

3.4 Bundle 大小与冷启动

见过一个真实案例:某公司把 500MB 的用户数据塞进 bundle,PDP 启动时拉包、解压、构建索引花了 40 秒。Kubernetes 滚动更新时,Pod 就绪探针要等 40 秒,批量扩容阶段整个集群可用性抖动。

经验法则:

3.5 Rego 学习曲线

团队落地 OPA 的第一个月,通常都会经历”写出来的策略 90% 是对的,剩下 10% 是诡异的 undefined”这种阶段。建议:

四、Cedar:AWS 的形式化验证方案

AWS 在 2023 年 re:Invent 开源 Cedar,同期推出 Amazon Verified Permissions 服务。Cedar 的目标非常明确:为 SaaS 应用提供一个”简单、安全、可验证”的授权语言。

4.1 设计哲学:牺牲表达力换可判定性

Cedar 的核心约束是:策略求值永远终止,且求值结果可以被形式化验证。这意味着 Cedar 不是图灵完备的——它没有递归、没有无限循环、没有外部调用。

这个约束带来的收益是巨大的:

这些在 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 有:

一个典型的实体 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 可以在策略编写时就发现:

这是 Cedar 相对 Rego 的重要优势:Rego 是动态类型的,这类错误要到运行时才能发现。

4.5 形式化验证

Cedar 背后有一套基于 Lean 4 的形式化证明,可以证明:

在 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-config

opa-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 云托管

托管方案的好处是省心,但要接受”所有授权查询都要过第三方”这个事实。金融、医疗、政府场景通常不选托管。

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.5

7.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

关键点:

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" }
  }
}

这份日志的价值巨大:

7.5 影响分析(Policy Diff)

发布新策略前,最好回答这个问题:这次改动会让哪些用户的哪些决策从 deny 变 allow,或反之

一种做法:把过去 24 小时的 decision log(input)灌进新旧两个 PDP,diff 结果。Cedar 在 AVP 里直接提供 PolicyAnalysis API,可以静态分析等价性,不需要真实数据。

7.6 变更评审

策略改动要当代码审:

八、工程坑点

8.1 Decision Log 体量

如果每个请求都落日志,大流量服务每天会产生几 TB。落地建议:

8.2 PDP 冷启动

几个应对方案:

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": [...] }
  }
}

这会导致:

更好的做法:每个租户一个独立 bundle,或用 OPA 的多 bundle 能力把 data root 按 tenant 分离。Cedar 的 PolicyStore 天然支持按租户分离。

8.5 策略里做 HTTP 调用

Rego 支持 http.send,可以在策略求值时调外部 API。这看起来很方便,但后果严重:

原则: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 分钟。

防御:

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 图灵完备)

几个常见误区:

落地路线图(针对一个中型团队的建议):

  1. 第 1 个月:选定引擎,搭最小 PDP(单 sidecar 或 daemon),迁移一个低风险服务的授权逻辑。
  2. 第 2 个月:建 CI 流水线,落地 opa test / cedar test,规范策略 PR 流程。
  3. 第 3 个月:接入 decision log,建审计看板。
  4. 第 4-6 个月:迁移核心服务,引入 Bundle 签名,做影响分析工具。
  5. 持续:策略治理委员会、定期策略 review、废弃无效策略。

十、参考资料


上一篇Zanzibar 风格权限系统

下一篇B2B SaaS 多租户权限设计

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-21 · architecture / security

【身份与访问控制工程】Zanzibar 风格权限系统

深入解析 Google Zanzibar 论文(USENIX ATC 2019)的核心设计:Relation Tuple、Namespace、Userset Rewrite,一致性模型(Zookies)与开源实现 SpiceDB、OpenFGA、Ory Keto 的工程对比,以及适用与不适用场景。

2026-04-21 · architecture / security

【身份与访问控制工程】B2B SaaS 多租户权限设计

B2B SaaS 的权限问题远比 B2C 复杂:多个企业客户、各自的内部角色体系、跨租户协作、行列级数据权限、租户自助管理。本文从隔离模型出发,给出租户内 RBAC + 租户间 ReBAC 的混合方案、超级管理员设计、行级权限实现,以及 GitHub、Slack、Notion 的权限模型速览。


By .