OPA 部署、Bundle 与决策日志工程手册
一次授权服务发布后,所有 OPA 实例的 Bundle 下载请求都返回了 200,值班人员因此判断策略已经更新;业务却仍按旧规则放行。后来从 Status 事件里才看到:新包已经下载,却在编译阶段失败,active_revision 一直停在旧值。下载、验证、编译和激活是四个不同状态,只盯 HTTP 成功会把“拿到文件”误当成“新决策已经生效”。
另一次事故更隐蔽。团队为 Decision Log 写了脱敏规则,配置加载也没有报错,日志平台里仍出现了请求 Token。原因是 mask policy 把原始请求错读成 input.token,而完整事件中的请求实际位于 input.input.token;同时,采集端只抽查了解压后的展示字段,没有扫描上传前的原始事件。配置存在不等于敏感字段已经消失,日志数量齐全也不等于它是一条 exactly-once 审计总线。
先把 OPA 放进正确的请求位置
OPA 是 Policy Decision Point,负责用 input + data + policy 计算 JSON 决策;应用、网关或代理才是 Policy Enforcement Point,负责认证调用者、构造输入、设置超时、解释决策并真正执行允许或拒绝。OPA 不会替应用关闭连接、回滚事务,也不会自动把 undefined 当成拒绝。PEP 必须把缺失结果、错误类型、超时和不可解析响应写成明确分支,授权入口通常选择 fail-close。
部署时最先决定的不是容器参数,而是决策调用跨过多少故障边界。
| 形态 | 请求路径 | 主要收益 | 主要代价 |
|---|---|---|---|
| 应用 sidecar | PEP 经回环或 Pod 网络调用同工作负载中的 OPA | 延迟低,策略副本贴近应用,短时失联仍可决策 | 每个副本都占 CPU、内存并保存 base data,升级扇出大 |
| 节点 daemon | 同节点工作负载调用一个 OPA agent | 副本数少于 sidecar,仍能避免跨节点访问 | 节点成为共享故障域,需要识别租户、限流和节点漂移 |
| 集中 service | 多个 PEP 通过服务网络访问 OPA 集群 | 资源集中,大数据只保留少量副本,发布面较小 | 网络延迟、服务发现和集中故障进入每次授权,必须做 HA |
| 进程内 SDK / Wasm | 决策与宿主同进程 | 最短调用链,可由宿主精细管理缓存 | 策略升级、运行时兼容与宿主发布耦合;Wasm 不带管理协议 |
Sidecar 不是天然正确答案。假设一份 base data 常驻内存为 D,单个 OPA 运行时及索引开销为 R,sidecar 数量为 N,仅副本成本就近似为 N × (D + R);集中 service 的副本成本较低,却增加网络 P99、连接池、队列和共享容量。选型前记录真实 bundle 解压后大小、激活峰值内存、决策 QPS、输入分布、P95/P99、更新频率和失联时长,不用一个通用 QPS 数字代替测量。
固定发行物,再打开监听端口
OPA 1.x 默认使用 Rego v1。安装时从 OPA Releases 选择团队审核的明确版本,核对官方摘要后再把版本写入镜像锁定文件或构建参数。下面的变量必须替换成该版本;edge 对应开发分支,不作为生产基线。
OPA_VERSION='<approved-version>'
curl -fL -o opa "https://openpolicyagent.org/downloads/v${OPA_VERSION}/opa_linux_amd64_static"
curl -fL -o opa.sha256 "https://openpolicyagent.org/downloads/v${OPA_VERSION}/opa_linux_amd64_static.sha256"
printf '%s %s\n' "$(cat opa.sha256)" opa | sha256sum -c -
chmod 0755 opa
./opa version容器入口同样固定版本,随后记录镜像解析出的 digest。官方镜像默认入口会执行 OPA,服务模式需要显式给出 run --server:
OPA_VERSION='<approved-version>'
docker pull "openpolicyagent/opa:${OPA_VERSION}"
docker image inspect "openpolicyagent/opa:${OPA_VERSION}" --format '{{index .RepoDigests 0}}'
docker run --rm "openpolicyagent/opa:${OPA_VERSION}" version
docker run --rm --name opa-lab -p 127.0.0.1:8181:8181 \
"openpolicyagent/opa:${OPA_VERSION}" \
run --server --addr=0.0.0.0:8181OPA 1.x server 默认监听 localhost:8181。容器内若仍使用默认监听,宿主端口映射存在也无法连接;显式绑定 0.0.0.0 只是解决可达性,不是完成安全加固。OPA 默认认证和授权都关闭,扩展到远程网络时必须同时配置 TLS、客户端身份、网络策略和 system.authz,并限制 Policy/Data 管理 API。PEP 通常只需要调用固定 decision path,不应获得上传策略或修改 base document 的能力。
先用健康端点证明进程可达,再用一个有默认拒绝的最小策略证明决策链可用:
package authz
default allow := false
allow if {
input.subject == "service-a"
input.action == "read"
}./opa fmt --fail authz.rego
./opa check --strict authz.rego
./opa run --server --addr=127.0.0.1:8181 authz.rego &
OPA_PID=$!
curl --fail-with-body -sS http://127.0.0.1:8181/health
curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d '{"input":{"subject":"service-a","action":"read"}}' \
http://127.0.0.1:8181/v1/data/authz/allow
# 预期:{"result":true}
kill "$OPA_PID"
wait "$OPA_PID" || true若返回 {},先按未定义决策处理,不要解释成 false。若健康检查成功而决策路径 404 或结果为空,检查 package、URL path、策略是否加载以及 PEP 请求是否带 input 外层对象。
Bundle 是一次有所有权的原子替换
生产运行不应依靠逐个调用 Policy/Data API 更新模块。Snapshot Bundle 把策略与 base data 打成 gzip tarball;成功下载后还要经过签名验证、解析、编译与激活,只有全部成功才会替换它声明拥有的根。.manifest 中最重要的字段是:
revision 是不透明版本标识,用来关联决策和激活状态,不保证一定是 Git SHA。roots 声明该 Bundle 拥有的 data 路径;缺省根意味着拥有全部 policy/data,多 Bundle 部署必须收窄并禁止重叠。rego_version 声明模块语义,OPA 1.x 新包使用 1;迁移包可用 file_rego_versions 做单文件声明,但重叠 pattern 必须在构建阶段拒绝。
wasm 与 metadata 分别描述 Wasm resolver 和附加元数据,不能替代 revision 与 roots。
一个可构建的最小目录如下:
bundle-src/
├── .manifest
├── authz.rego
└── roles.json{
"revision": "R1",
"roots": ["authz", "roles"],
"rego_version": 1
}./opa build --bundle bundle-src --output authz-R1.tar.gz
./opa inspect authz-R1.tar.gz
./opa eval --bundle authz-R1.tar.gz \
--stdin-input 'data.authz.allow' \
<<< '{"subject":"service-a","action":"read"}'上线包应使用测试专用私钥离线签名,OPA consumer 只持有公钥。签名是 opt-in:包内有 .signatures.json 但 agent 没有 signing 配置会失败;agent 要求 signing 而包未签也会失败;只有两边都配置且文件摘要、key ID、scope 全部通过才可激活。签名提供来源和完整性,不提供机密性、可信时间、业务审批或防旧包重放;revision 和 ETag 也不是回滚保护器。
配置服务、Bundle 拉取、持久化和 Status 上报时,可以从下面的结构起步。字段值要按容量测试和目标版本的 OPA Configuration Reference 固定,不直接照抄示例为生产阈值。
services:
policy_api:
url: https://policy.example.com
credentials:
bearer:
token_path: /var/run/secrets/opa/policy-api-token
keys:
bundle-prod:
algorithm: RS256
key: ${OPA_BUNDLE_PUBLIC_KEY}
bundles:
authz:
service: policy_api
resource: bundles/authz.tar.gz
persist: true
signing:
keyid: bundle-prod
scope: authz
status:
service: policy_api
partition_name: region-a
decision_logs:
service: policy_api
resource: /logs/authz
mask_decision: /system/log/maskpersist: true 只保存成功激活的 snapshot Bundle,默认落在工作目录的 .opa/bundles/<name>/bundle.tar.gz,用于控制服务失联时 best-effort 重启恢复。容器不挂持久卷,重建后文件就会消失;它不保存临时 REST 写入、decision cache、日志队列或 delta Bundle。Delta 只更新 data,不能更新 policy,且当前签名与持久化能力不同于 snapshot,不能拿 snapshot 的实验结果替它背书。
Discovery 只从受信 bootstrap 长出配置
当大量 agent 需要按环境、区域或租户获得不同 Bundle、Status 与 Decision Log 配置时,Discovery 可以减少静态配置复制。进程启动时仍必须有 bootstrap:控制服务地址、访问凭证、labels、Discovery resource 以及验证 Discovery Bundle 的公钥。OPA 拉取并验证 Discovery Bundle,再求值配置决策,生成普通插件配置。
这条链路有意阻止“远端配置自己改掉信任锚”:Discovery 不能修改自己的 service,也不能下发验证自身的公钥;bootstrap 中与 discovered configuration 冲突的插件配置由 bootstrap 胜出,bootstrap labels 不能被覆盖。Discovery Bundle 应签名,因为它可能携带普通 Bundle 的验证 key 或服务配置。
services:
bootstrap:
url: https://policy.example.com
credentials:
bearer:
token_path: /var/run/secrets/opa/bootstrap-token
labels:
region: region-a
runtime: sidecar
keys:
discovery-root:
algorithm: RS256
key: ${OPA_DISCOVERY_PUBLIC_KEY}
discovery:
service: bootstrap
resource: discovery/region-a.tar.gz
persist: true
signing:
keyid: discovery-root断网重启实验要分两次做:保留持久化 Discovery Bundle 时,OPA 可以 best-effort 重新求值配置;删除持久文件后再重启,则不应假设还有上次配置。若远端试图修改 Discovery 自己,第一证据是 Discovery plugin 的 activation error 与实际 bootstrap,而不是反复发布同一个包。
Status 要证明激活,不只是证明拉取
每个 agent 上报的 Status 至少关联 labels、插件状态、active_revision、最近成功下载、最近成功激活、错误 code/message、编译错误和 HTTP 状态。读取它时按状态机判断:
发现目标 revision
-> 下载成功
-> 签名与摘要验证成功
-> parse / compile 成功
-> roots 无冲突
-> 原子激活
-> active_revision 收敛
-> 决策样本命中新行为正向发布的完成判据不是所有实例“最近有成功请求”,而是目标实例集合的 active_revision 全部收敛、activation error 为零、业务 canary 决策符合预期,并且旧 revision 的数量持续下降到允许值。/health?bundles=true 可以把 Bundle 状态纳入健康检查,但 stale-policy 是否仍可接流量取决于业务预算;授权撤销要求很短的新鲜度时,单纯“最后一个已知好包”可能已不再安全。
反向实验先激活 R1,再把 R2 中一段 Rego 改坏或破坏签名。预期是下载状态可能成功,激活失败,active_revision 仍为 R1,决策继续保持 R1 结果。若出现一部分模块按 R2 工作、一部分仍为 R1,说明发布路径绕开了 Bundle 原子激活或 PEP 在不同决策端点混用了版本。
Decision Log 先脱敏,再讨论送达
Decision Log 事件可能包含 decision ID、labels、Bundle revision、decision path、完整 input、result、metrics 和允许采集的请求上下文。脱敏策略在 OPA 内、编码与上传之前执行;mask policy 的 input 是完整日志事件,所以业务请求位于 input.input。
package system.log
mask contains "/input/password" if {
input.input.resource == "user"
}
mask contains {"op": "upsert", "path": "/input/token", "value": "**REDACTED**"} if {
input.input.token
}简单 JSON Pointer 表示删除,结构化结果可执行 remove 或 upsert。可处理路径必须从 /input、/result 或 /nd_builtin_cache 开始,不存在的路径会被忽略。正向实验向决策端点发送 password、token 和普通字段,直接检查 collector 解压后的原始 gzip 事件:password 不存在,token 为固定替代值,erased/masked 路径正确,普通字段、decision ID 与 revision 仍存在。
反向实验把 pointer 改成 /password,或者把条件改成读取 input.password。OPA 仍可启动,collector 却能看到原值,这正是要稳定捕获的失败。另一个反例令 data.system.log.drop 错误匹配拒绝事件:collector 完全收不到该事件,业务 deny 计数与审计事件数开始分离。两条曲线的差值、buffer/rate drop、collector 非 2xx、重试年龄和进程退出前未发送事件都要有指标。
Decision Log 不是 exactly-once 总线。非 2xx 会触发重排和退避,但 buffer 上限、速率限制、超大事件、drop policy 与进程退出仍会造成丢失。对强审计要求,PEP 自己保存不可变业务审计记录,OPA 日志提供策略解释和 revision 关联;不要让一条有损遥测链独自承担法定留痕。
把策略运行时接进项目与 CI
项目仓库保存 Rego、测试、Bundle manifest 与构建脚本,不保存生产 Token、私钥或真实请求样本。CI 的顺序应是格式化检查、严格编译、单元测试、Bundle 构建、Bundle inspect、签名、发布到不可变 resource,最后才更新环境指针。一个最小流水线步骤可以直接调用官方 CLI:
set -euo pipefail
opa fmt --fail policy/
opa check --strict policy/
opa test --coverage --format=json policy/ > opa-test.json
opa build --bundle policy/ --output authz.tar.gz
opa inspect authz.tar.gz > opa-inspect.txt部署清单只引用审核过的镜像 digest、只读配置和 Secret volume;readiness 查询 Bundle 健康,liveness 只判断进程是否需要重启,避免控制服务短暂失联引发所有 sidecar 同时重启。PEP 请求固定 decision path,携带 request ID,并将 OPA decision ID、active revision、延迟与最终执行动作关联起来。这样 allow=true 但业务仍拒绝时,可以分辨是策略输出、PEP 解释还是下游执行失败。
权限至少拆成四组:Bundle producer 可以构建和签名但不能改 agent bootstrap;发布者可以写 Bundle 仓库但拿不到签名私钥;OPA agent 只读指定 resource 并写 Status/Decision Log;PEP 只能调用获批决策路径。Status、metrics 和 health 也可能暴露拓扑、revision 与错误内容,不能因是观测端点就直接公开。
容量、HA 与升级必须一起设计
容量测试要同时覆盖稳定流量和发布尖峰。激活新 Bundle 时,下载压缩包、解压、解析、编译并建立内存状态可能与旧版本短时共存;只测稳定态 RSS 会低估峰值。对每种部署形态记录:
Bundle 压缩与解压大小、模块数、base data 大小、激活耗时和峰值 RSS。决策输入大小、QPS、并发、P50/P95/P99、超时与拒绝比例。Status 与 Decision Log 出站带宽、buffer 使用量、drop 数和 collector 退避年龄。
控制服务失联、DNS 失败、证书轮换、全量实例同时轮询时的恢复曲线。
Sidecar 通过错峰轮询和发布分批降低 Bundle 服务惊群;集中 service 需要至少跨故障域副本、连接池、过载拒绝、滚动摘流和 PEP 侧总 deadline。节点 daemon 要防一个租户的大输入或昂贵规则拖慢整节点,并为节点排空、daemon 重启和工作负载迁移保留证据。
升级先在 canary agent 上加载现网 Bundle 与固定请求集,比较旧新二进制的 parse/compile、decision JSON、undefined、built-in error、延迟和内存。随后只扩大 agent 比例,不同时更改 Rego、Bundle roots、Discovery 和日志策略。回滚优先回到上一审核镜像,同时保持最后成功 snapshot Bundle;如果新 Bundle 已使用旧运行时不支持的语法,必须同步把环境指针回退到兼容 revision。
多副本 service 的回滚不能只看容器版本。PEP 返回或日志中必须带 active revision,直到所有接流量副本都回到目标组合;否则同一请求重试可能在新旧策略间振荡。升级期间如果 producer 与 consumer 跨 Rego 语义迁移,先让 producer 在 manifest 明确 rego_version,再升级 consumers,最后移除过渡兼容开关。
故障证据从状态分层开始
遇到异常时先判断失败在哪一层,而不是立刻重启:
| 现象 | 第一证据 | 常见原因 | 修复后证明 |
|---|---|---|---|
| 宿主或其他容器连不上 | 实际监听地址、端口映射、socket | OPA 仍只监听容器内 localhost | 外部入口在受控网络可达,管理 API 仍受保护 |
| Bundle 下载成功但决策未变 | download/activation 时间、errors、active revision | 编译、签名、scope 或 roots 失败 | revision 收敛且 canary 决策变化 |
| 重建后策略丢失 | persist、工作目录、volume、持久文件 | 未挂卷、只接收 delta、目录无写权 | 断开 Bundle 服务重启仍恢复最后 snapshot |
| 多 Bundle 间歇报错 | 每个 manifest 的 roots 与 plugin status | roots 重叠且没有加载顺序保证 | 所有权拆开,重复发布不再产生 activation error |
| 日志泄漏敏感字段 | collector 原始 gzip 事件 | pointer 或事件层级写错 | 正反样本扫描均不出现原值 |
| 日志少于业务决策 | drop/rate/buffer 指标与 collector HTTP code | 主动 drop、溢出、退避、进程退出 | 差值可解释并在预算内,超限告警触发 |
重启可能清掉内存队列、抹去插件错误并扩大日志缺口,因此应先保存 Status、metrics、进程参数、镜像 digest、配置摘要、Bundle revision 与一组脱敏决策样本。修复完成后重复原始反例,不能用一个新的健康请求代替故障复现。
可逆退出比删除 Deployment 多几步
退出 sidecar 时先让 PEP 支持另一条决策路径或本地降级实现,影子比较两边结果与原因码;确认新路径容量和失败语义后分批切流,再停止旧 OPA。退出集中 service 时还要撤销客户端凭证、DNS 和网络入口,归档最后可验证 Bundle、manifest、公钥、策略测试与决策 schema,按保留制度清理 Decision Log。
停止 Discovery 与 Bundle 服务前,先盘点所有 agent 的最近心跳和 active revision,防止遗留实例长期携带旧策略运行。删除持久卷前证明没有回滚依赖;删除 signing key 前保留历史公钥与制品验证链,私钥则按密钥销毁流程处理。最后核销负载均衡、日志索引、对象存储、监控、Secret、服务账号和证书,让“请求已经不再经过 OPA”与“OPA 资产已经清除”分别有证据。
