本文为 SPIRE 官方文档 authorization_policy_engine.md 的中文译本,以英文原文为准。 🔗 穿越源码分析:整体架构 · 方法级授权
授权策略引擎
警告:使用自定义授权策略属于实验性(experimental)功能,如果配置不当,可能导致安全性下降。关于扩展默认策略的更多细节,请参阅本节。
SPIRE 中的授权决策由一个策略引擎(policy engine)决定,该引擎借助 Open Policy Agent(OPA),基于一份 rego 策略和数据绑定(databindings)来做出决策。
下面是该策略的一个示例配置。
server {
experimental {
auth_opa_policy_engine {
local {
rego_path = "./conf/server/policy.rego"
policy_data_path = "./conf/server/policy_data.json"
}
}
}
}如果没有设置策略引擎配置,它会默认采用默认 SPIRE 授权策略。
策略引擎详解
策略引擎基于 Open Policy Agent(OPA)。它通过两个部分进行配置:rego 策略,以及策略数据路径(policy data path,即 OPA 中所称的 databindings)。
- rego 策略是一个 rego 策略文件,定义如何对 API 调用进行授权。
- 策略数据(或称 databindings)是一段 JSON 数据,定义可在 rego 策略中使用的额外数据。
总体而言,策略的哪些方面可以放进 rego、哪些可以放进 databindings,二者存在一定重叠。不过一般规则是:"如何做(How it is done)"属于 rego 策略,而"这适用于什么(What does this apply to)"属于 databindings 文件。
Rego 策略
rego 策略定义了如何对策略引擎的输入进行求值,以产生 SPIRE server 用于授权决策的 result(结果)。
它由 result 对象定义:
result = {
"allow": true/false,
"allow_if_admin": true/false,
"allow_if_local": true/false,
"allow_if_downstream": true/false,
"allow_if_agent": true/false,
}result 的各字段如下:
allow:布尔值,若为 true,则授权该调用allow_if_local:布尔值,若为 true,则仅当调用方是本地 UNIX 套接字(socket)调用时才授权该调用allow_if_admin:布尔值,若为 true,则仅当调用方是设置了 Admin 标志的 SPIFFE ID 时才授权该调用allow_if_downstream:布尔值,若为 true,则仅当调用方是下游(downstream)的 SPIFFE ID 时才授权该调用allow_if_agent:布尔值,若为 true,则仅当调用方是 agent 时才授权该调用。
这些结果按以下语义进行求值,其中 isX() 表示对调用方是否具有属性 X 的判定。
admit_request =
allow || (allow_if_local && isLocal()) || (allow_if_admin && isAdmin()) ||
(allow_if_downstream && isDownstream()) || (allow_if_agent && isAgent())传入策略的输入(input)如下:
input:来自 SPIRE server、针对该授权调用的输入data:来自策略数据文件的 databinding
| input 字段 | 说明 | 示例 |
|---|---|---|
| caller | 调用方的 SPIFFE ID(若可用) | spiffe://example.org/workload1 |
| caller_file_path | 调用方的二进制路径(若可用) | /spire-controller-manager |
| full_method | 基于 SPIRE API 的该 API 调用的完整方法名 | /spire.api.server.svid.v1.SVID/MintJWTSVID |
| req | API 调用的请求体(在客户端流式或双向流式 RPC 调用中不可用) | { "filter": |
请求(req)是来自 SPIRE api sdk 的、经序列化(marshalled)的 JSON 对象。注意,它在客户端流式或双向流式 RPC API 调用中不可用。
策略数据文件(databinding)
策略数据文件由一段 JSON 数据构成,表示在策略求值中所用的数据。它通常是自由格式的,可以在 rego 策略中以任意方式使用。这段 JSON 数据中的内容,会在策略引擎求值时被预编译进策略求值过程中。因此,建议尽可能多地把数据放入 databinding,以便策略引擎对其进行优化。
这些数据对象可以通过 rego 策略中的 data 字段访问。例如,一个 JSON 数据对象可能长这样:
{
"apis": [
{ "full_method": "/spire.api.server.svid.v1.SVID/MintJWTSVID" },
{ "full_method": "/spire.api.server.bundle.v1.Bundle/GetFederatedBundle"},
{ "full_method": "/spire.api.server.svid.v1.SVID/BatchNewX509SVID"}
]
}有了上面的示例数据对象,我们就可以用 rego 构造一个策略,来检查:如果 input 的完整方法名等于 apis 字段中某个对象的 full_method 子字段,那么就应把 allow 设为 true。
allow = true {
input.full_method == data.apis[_].full_method
}默认配置
下面是默认的 rego 策略与策略数据取值。它们是执行默认 SPIRE 授权决策所必需的。
默认的 policy.rego
默认的 rego 策略位于 policy.rego。
默认的 policy_data.json(databindings)
默认的 policy_data.json 位于 policy_data.json。
默认的策略数据文件包含一个名为 "apis" 的字段。 该字段包含一个当前正由 rego 策略配置的 API 列表。
每个对象的字段如下:
| 字段 | 说明 | 示例 |
|---|---|---|
| full_method | 该 API 调用的完整方法名 | /spire.api.server.svid.v1.SVID/MintJWTSVID |
| allow_any | 若为 true,则把 result.allow 设为 true | |
| allow_local | 若为 true,则把 result.allow_if_local 设为 true | |
| allow_admin | 若为 true,则把 result.allow_if_admin 设为 true | |
| allow_downstream | 若为 true,则把 result.allow_if_downstream 设为 true | |
| allow_agent | 若为 true,则把 result.allow_if_agent 设为 true |
扩展策略
本节包含若干示例,演示如何扩展授权策略。
OPA 警告
在实现自定义策略时,理解 OPA rego 的求值语义与细节非常重要。OPA rego 策略微妙之处的一个例子是:对一个变量的求值,被视为其所有子句(clause)的逻辑或(OR)。因此,新增一条把 allow = false 的规则,并不能有效地增强策略。
在实现自定义策略之前,建议先熟悉 OPA rego 语言。
示例 1a:注册条目创建的命名空间限制
在这个示例中,我们希望确保所创建的条目带有命名空间划分,这样就能在信任域(trust domain)内创建命名空间,以决定每个客户端可以创建哪类条目。这可以对应这样一个场景:有两个部门,其中一个不能为另一个创建条目。
注意,这个示例专门针对经由 TCP 端点发起的调用,此时用户对应于调用 API 时所出示的 x509 证书中的 SPIFFE ID。
可以通过在 data binding 中新建一些额外对象来定义:
{
"entry_create_namespaces": [
{
"user": "spiffe://example.org/schedulers/finance",
"path_namespace": "^/finance"
},
{
"user": "spiffe://example.org/schedulers/hr",
"path_namespace": "^/hr"
}
]
}随后可以更新 rego 策略,让它拿用户命名空间与路径前缀的数据集,与创建条目的输入请求进行比对。
check_entry_create_namespace {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
# caller has the registrar role
b = data.entry_create_namespaces[_]
b.user == input.caller
# spiffe id to be registered is in correct namespace
re_match(b.path_namespace, input.req.entries[_].spiffe_id.path)
}随后可以更新 rego 策略来做这项检查,一个 allow 子句的示例如下所示。注意,务必检查它与 rego 策略其他部分如何配合。
# Any allow check
allow = true {
check_entry_create_namespace
}示例 1b:带排除项的子部门命名空间划分
在上一个示例的基础上,假设我们希望设立子部门,让调度器(scheduler)负责信任域内路径的一个子集。这可以在上一个示例的基础上,通过增加一个排除列表(exclusion list)来实现。
在这个示例中,我们有两个调度器:
schedulers/finance能够创建以/finance开头的路径schedulers/finance/EMEA能够创建以/finance/EMEA开头的路径schedulers/finance不应能够创建以/finance/EMEA开头的路径
为此,我们可以沿用上面相同的策略,再加上一个排除列表。我们将使用以下策略数据:
{
"entry_create_namespaces": [
{
"user": "spiffe://example.org/schedulers/finance",
"path_namespace": "^/finance",
"path_exclusions": [
"^/finance/EMEA"
]
},
{
"user": "spiffe://example.org/schedulers/finance/EMEA",
"path_namespace": "^/finance/EMEA"
}
]
}然后我们可以增加几行,来检查排除列表:
check_entry_create_namespace {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
# caller has the registrar role
b = data.entry_create_namespaces[_]
b.user == input.caller
# spiffe id to be registered is in correct namespace
re_match(b.path_namespace, input.req.entries[_].spiffe_id.path)
# check if the spiffe id to be registered doesn't hit an exclusion
exclusions := b.path_exclusions
exclusion_matches := { entry | entry := input.req.entries[_]; re_match(exclusions[_], entry.spiffe_id.path)}
count(exclusion_matches) == 0
}
check_entry_create_namespace {
input.full_method != "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
}这样,期望的布尔结果就会被存入 check_entry_create_namespace。
示例 2:禁止在创建条目时使用 admin 标志
在第二个示例中,我们希望加以限制,阻止创建任何带 admin 标志的条目。这可以通过用以下检查修改 rego 策略的 allow 子句来实现:
check_entry_create_admin_flag {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchCreateEntry"
admin_entries := { entry | entry := input.req.entries[_]; entry.admin == true}
count(admin_entries) == 0
}这会在以下情况把 check_entry_create_admin_flag 设为 true:完整方法名不是创建条目;或者即便是创建条目,也没有任何条目包含 admin 标志。
随后可以更新 rego 策略来做这项检查,一个 allow 子句的示例如下所示。注意,务必检查它与 rego 策略其他部分如何配合。
# Any allow check
allow = true {
check_entry_create_admin_flag
}示例 3a:限制来自本地 UNIX 套接字的调用
在这个示例中,我们希望限制条目的删除。在本示例的第一部分,我们将完全锁死删除条目的能力。
借助那组默认规则可以轻松做到这一点。在默认的策略数据文件中,针对各 API 有一般性的 allow 限制。例如,对于批量删除条目,以下是相应的摘录:
{
"full_method": "/spire.api.server.entry.v1.Entry/BatchDeleteEntry",
"allow_admin": true,
"allow_local": true
}如果我们想禁止本地用户或 admin 用户删除条目,只需删除 allow* 相关行即可,得到:
{
"full_method": "/spire.api.server.entry.v1.Entry/BatchDeleteEntry",
}示例 3b:允许特定用户执行删除
在这个示例中,我们现在想放宽之前的限制,允许某一个 SPIFFE ID 经由 TCP 端点执行删除。
我们可以先定义 data binding,给出能够删除条目的用户列表:
{
"entry_delete_users": [
"spiffe://example.org/finance/super-admin-deleter",
"spiffe://example.org/hr/super-admin-deleter"
]
}然后我们可以定义以下 rego 策略,来检查对删除条目端点的调用,并加入对"调用方 SPIFFE ID 是否在所定义用户列表中"的检查。
check_entry_delete_users {
input.full_method == "/spire.api.server.entry.v1.Entry/BatchDeleteEntry"
# caller has the registrar role
input.caller == data.entry_delete_users[_]
}随后可以更新 rego 策略来做这项检查,一个 allow 子句的示例如下所示。注意,务必检查它与 rego 策略其他部分如何配合。
# Any allow check
allow = true {
check_entry_delete_users
}