Skip to main content
Version: 3.17

opa

描述#

opa 插件支持与 Open Policy Agent (OPA) 集成,OPA 是一个统一的策略引擎和框架,可用于定义和执行授权策略。授权逻辑以 Rego 语言编写并存储在 OPA 中。

配置后,OPA 引擎将根据定义的策略评估客户端对受保护路由的请求,以决定是否允许其访问上游资源。

属性#

名称类型必选项默认值有效值描述
hoststringOPA 服务器地址。
policystring要评估的策略路径。该值会被直接拼接到 OPA 的 /v1/data/<policy>data.result 必须是一个包含 allow 字段的对象,并可按需包含 reasonheadersstatus_code 等字段;因此,policy 应指向返回该对象结构的路径,而不是仅返回 truefalse 的布尔规则。
ssl_verifybooleantrue若为 true,则验证 OPA 服务器的 SSL 证书。
timeoutinteger3000[1, 60000]HTTP 调用超时时间(毫秒)。
keepalivebooleantrue若为 true,则为多个请求保持连接活跃。
keepalive_timeoutinteger60000>= 1000连接空闲后关闭的等待时间(毫秒)。
keepalive_poolinteger5>= 1空闲连接数。
with_routeboolean若为 true,发送当前路由的信息。
with_serviceboolean若为 true,发送当前 Service 的信息。
with_consumerboolean若为 true,发送当前消费者的信息。注意,Consumer 信息可能包含 API key 等敏感信息,仅在确认安全的情况下将此选项设为 true
send_headers_upstreamarray[string]>= 1请求被允许时,需要从 OPA 响应转发到上游服务的请求头名称列表。

数据定义#

APISIX 向 OPA 发送信息#

下述示例展示了 APISIX 向 OPA 服务发送的数据格式:

{
"type": "http",
"request": {
"scheme": "http",
"path": "\/get",
"headers": {
"user-agent": "curl\/7.68.0",
"accept": "*\/*",
"host": "127.0.0.1:9080"
},
"query": {},
"port": 9080,
"method": "GET",
"host": "127.0.0.1"
},
"var": {
"timestamp": 1701234567,
"server_addr": "127.0.0.1",
"server_port": "9080",
"remote_port": "port",
"remote_addr": "ip address"
},
"route": {},
"service": {},
"consumer": {}
}

各字段说明如下:

  • type 表示请求类型(httpstream)。
  • requesttypehttp 时使用,包含基本的请求信息(URL、请求头等)。
  • var 包含请求连接的基本信息(IP、端口、请求时间戳等)。
  • routeserviceconsumer 包含与 APISIX 中存储的相同数据,仅在这些对象上配置了 opa 插件时才会发送。

OPA 向 APISIX 返回数据#

下述示例展示了 OPA 服务对 APISIX 的响应数据格式:

{
"result": {
"allow": true,
"reason": "test",
"headers": {
"an": "header"
},
"status_code": 401
}
}

各字段说明如下:

  • allow 是必填字段,表示请求是否允许通过 APISIX 转发。
  • reasonheadersstatus_code 是可选字段,仅在配置了自定义响应时返回。

使用示例#

note

你可以这样从 conf/config.yaml 中获取 admin_key 并存入环境变量:

admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')

在开始之前,你需要一个运行中的 OPA 服务器。可以通过 Docker 启动或部署到 Kubernetes:

docker run -d --name opa-server -p 8181:8181 openpolicyagent/opa:1.6.0 run --server --addr :8181 --log-level debug

验证 OPA 服务器安装正常且端口已正确暴露:

curl http://127.0.0.1:8181 | grep Version

你应该看到类似如下的响应:

Version: 1.6.0

实现基本策略#

以下示例在 OPA 中实现一个仅允许 GET 请求的基本授权策略。

创建一个仅允许 HTTP GET 请求的 OPA 策略:

curl "http://127.0.0.1:8181/v1/policies/getonly" -X PUT  \
-H "Content-Type: text/plain" \
-d '
package getonly

default allow = false

allow if {
input.request.method == "GET"
}'

创建带有 opa 插件的路由:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "opa-route",
"uri": "/anything",
"plugins": {
"opa": {
"host": "http://127.0.0.1:8181",
"policy": "getonly"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

host 替换为你的 OPA 服务器地址,policy 设置为 getonly

向路由发送 GET 请求以验证策略:

curl -i "http://127.0.0.1:9080/anything"

你应该收到 HTTP/1.1 200 OK 响应。

使用 PUT 方法向路由发送请求:

curl -i "http://127.0.0.1:9080/anything" -X PUT

你应该收到 HTTP/1.1 403 Forbidden 响应。

了解数据格式#

以下示例帮助你了解 APISIX 推送给 OPA 的数据格式,以便编写授权逻辑。该示例基于上一个示例中的策略和路由。

更新之前创建的路由上的插件,使其包含路由信息:

curl "http://127.0.0.1:9180/apisix/admin/routes/opa-route" -X PATCH \
-H "X-API-KEY: ${admin_key}" \
-d '{
"plugins": {
"opa": {
"with_route": true
}
}
}'

向路由发送请求:

curl -i "http://127.0.0.1:9080/anything"

在 OPA 服务器日志(启用 --log-level debug)中,req_body 将在请求和变量字段之外还包含路由信息。

返回自定义响应#

以下示例演示如何在请求未授权时返回自定义响应码和消息。

创建一个仅允许 HTTP GET 请求、并在未授权时返回 302 及自定义消息的 OPA 策略:

curl "http://127.0.0.1:8181/v1/policies/customresp" -X PUT \
-H "Content-Type: text/plain" \
-d '
package customresp

default allow = false

allow if {
input.request.method == "GET"
}

reason := "The resource has temporarily moved. Please follow the new URL." if {
not allow
}

headers := {
"Location": "http://example.com/auth"
} if {
not allow
}

status_code := 302 if {
not allow
}
'

创建带有 opa 插件的路由:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "opa-route",
"uri": "/anything",
"plugins": {
"opa": {
"host": "http://127.0.0.1:8181",
"policy": "customresp"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

向路由发送 GET 请求:

curl -i "http://127.0.0.1:9080/anything"

你应该收到 HTTP/1.1 200 OK 响应。

向路由发送 POST 请求:

curl -i "http://127.0.0.1:9080/anything" -X POST

你应该收到 HTTP/1.1 302 Moved Temporarily 响应:

HTTP/1.1 302 Moved Temporarily
...
Location: http://example.com/auth

The resource has temporarily moved. Please follow the new URL.

实现 RBAC#

以下示例演示如何结合 jwt-authopa 插件实现认证和 RBAC,其中:

  • user 角色仅能读取上游资源。
  • admin 角色可以读取和写入上游资源。

为示例消费者 johnuser 角色)和 janeadmin 角色)创建 OPA RBAC 策略:

curl "http://127.0.0.1:8181/v1/policies/rbac" -X PUT \
-H "Content-Type: text/plain" \
-d '
package rbac

# Assign roles to users
user_roles := {
"john": ["user"],
"jane": ["admin"]
}

# Map permissions to HTTP methods
permission_methods := {
"read": "GET",
"write": "POST"
}

# Assign role permissions
role_permissions := {
"user": ["read"],
"admin": ["read", "write"]
}

# Get JWT authorization token
bearer_token := t if {
t := input.request.headers.authorization
}

# Decode the token to get role and permission
token := {"payload": payload} if {
[_, payload, _] := io.jwt.decode(bearer_token)
}

# Normalize permission to a list
normalized_permissions := ps if {
ps := token.payload.permission
not is_string(ps)
}

normalized_permissions := [ps] if {
ps := token.payload.permission
is_string(ps)
}

# Implement RBAC logic
default allow = false

allow if {
# Look up the list of roles for the user
roles := user_roles[input.consumer.username]

# For each role in that list
r := roles[_]

# Look up the permissions list for the role
permissions := role_permissions[r]

# For each permission
p := permissions[_]

# Check if the permission matches the request method
permission_methods[p] == input.request.method

# Check if the normalized permissions include the permission
p in normalized_permissions
}
'

在 APISIX 中创建消费者 johnjane,并配置 jwt-auth 凭证:

curl "http://127.0.0.1:9180/apisix/admin/consumers" \
-X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"username": "john"
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" \
-X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"username": "jane"
}'

为消费者配置 jwt-auth 凭证,使用默认算法 HS256

curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "cred-john-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "john-key",
"secret": "john-hs256-secret-that-is-very-long"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "cred-jane-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "jane-key",
"secret": "jane-hs256-secret-that-is-very-long"
}
}
}'

创建路由并配置 jwt-authopa 插件:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "opa-route",
"methods": ["GET", "POST"],
"uris": ["/get","/post"],
"plugins": {
"jwt-auth": {},
"opa": {
"host": "http://127.0.0.1:8181",
"policy": "rbac",
"with_consumer": true
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

john 身份验证#

要为 john 生成 JWT,可使用 JWT.io 的 JWT 编码器 或其他工具。若使用 JWT.io 的 JWT 编码器,请执行以下操作:

  • 将算法填写为 HS256
  • Valid secret 部分的密钥更新为 john-hs256-secret-that-is-very-long
  • 在 payload 中填入角色 user、权限 read、Consumer key john-key,以及 UNIX 时间戳格式的 expnbf

你的 payload 应类似如下:

{
"role": "user",
"permission": "read",
"key": "john-key",
"nbf": 1729132271
}

复制生成的 JWT 并存入变量:

export john_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInBlcm1pc3Npb24iOiJyZWFkIiwia2V5Ijoiam9obi1rZXkiLCJuYmYiOjE3MjkxMzIyNzF9.rAHMTQfnnGFnKYc3am_lpE9pZ9E8EaOT_NBQ5Ss8pk4

使用 john 的 JWT 向路由发送 GET 请求:

curl -i "http://127.0.0.1:9080/get" -H "Authorization: Bearer ${john_jwt_token}"

你应该收到 HTTP/1.1 200 OK 响应。

使用相同 JWT 向路由发送 POST 请求:

curl -i "http://127.0.0.1:9080/post" -X POST -H "Authorization: Bearer ${john_jwt_token}"

你应该收到 HTTP/1.1 403 Forbidden 响应。

jane 身份验证#

同样地,为 jane 生成 JWT,可使用 JWT.io 的 JWT 编码器 或其他工具。若使用 JWT.io 的 JWT 编码器,请执行以下操作:

  • 将算法填写为 HS256
  • Valid secret 部分的密钥更新为 jane-hs256-secret-that-is-very-long
  • 在 payload 中填入角色 admin、权限 ["read","write"]、Consumer key jane-key,以及 UNIX 时间戳格式的 expnbf

你的 payload 应类似如下:

{
"role": "admin",
"permission": ["read","write"],
"key": "jane-key",
"nbf": 1729132271
}

复制生成的 JWT 并存入变量:

export jane_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjpbInJlYWQiLCJ3cml0ZSJdLCJrZXkiOiJqYW5lLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.meZ-AaGHUPwN_GvVOE3IkKuAJ1wqlCguaXf3gm3Ww8s

使用 jane 的 JWT 向路由发送 GET 请求:

curl -i "http://127.0.0.1:9080/get" -H "Authorization: Bearer ${jane_jwt_token}"

你应该收到 HTTP/1.1 200 OK 响应。

使用相同 JWT 向路由发送 POST 请求:

curl -i "http://127.0.0.1:9080/post" -X POST -H "Authorization: Bearer ${jane_jwt_token}"

你也应该收到 HTTP/1.1 200 OK 响应。