OPA
The opa
plugin supports the integration with Open Policy Agent (OPA), a unified policy engine and framework that helps defining and enforcing authorization policies. Authorization logics are defined in Rego and stored in OPA.
Once configured, the OPA engine will evaluate the client request to a protected route to determine whether the request should have access to the upstream resource based on the defined policies.
Examples
The examples below demonstrate how you can work with the opa
plugin for different scenarios.
Before proceeding, you should have a running OPA server, or start a new one in Docker:
docker run -d -p 8181:8181 --name opa openpolicyagent/opa run -s --log-level debug
run -s
starts OPA as a server.--log-level debug
prints debug information to examine the data APISIX pushes to OPA.
Implement a Basic Policy
The following example implements a basic authorization policy in OPA to allow only GET requests.
Create an OPA policy that only allows HTTP GET requests:
curl "http://127.0.0.1:8181/v1/policies/getonly" -X PUT \
-H "Content-Type: text/plain" \
-d '
package getonly
import input.request
default allow = false
allow {
request.method == "GET"
}'
Create a route with the opa
plugin as such:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "opa-route",
"uri": "/anything",
"plugins": {
"opa": {
"host": "http://192.168.2.104:8181",
"policy": "getonly"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
❶ Configure the OPA server address. Replace with your IP address.
❷ Set the authorization policy to be getonly
.
To verify the policy, send a GET request to the route:
curl -i "http://127.0.0.1:9080/anything"
You should receive an HTTP/1.1 200 OK
response.
Send another request to the route using PUT:
curl -i "http://127.0.0.1:9080/anything" -X PUT
You should receive an HTTP/1.1 403 Forbidden
response.
Understand Data Format
The following example helps you understand the data and the format APISIX pushes to OPA to support authorization logic writing. The example continues with the policy and the route in the last example
Suppose your OPA server is started with --log-level debug
and you have completed the verification steps in the last example sending requests to the sample route.
Navigate to the OPA server log. You should see an entry similar to the following:
{
"client_addr": "192.168.65.1:33640",
"level": "info",
"msg": "Received request.",
"req_body": "{\"input\":{\"var\":{\"remote_addr\":\"192.168.65.1\",\"server_addr\":\"192.168.224.3\",\"remote_port\":\"20939\",\"server_port\":\"9080\",\"timestamp\":1725260108},\"request\":{\"path\":\"/anything\",\"port\":9080,\"method\":\"GET\",\"scheme\":\"http\",\"host\":\"127.0.0.1\",\"query\":{},\"headers\":{\"host\":\"127.0.0.1:9080\",\"user-agent\":\"curl/8.6.0\",\"accept\":\"*/*\"}},\"type\":\"http\"}}",
"req_id": 3,
"req_method": "POST",
"req_params": {},
"req_path": "/v1/data/getonly",
"time": "2024-09-02T06:55:08Z"
}
where the req_body
shows the data APISIX pushed:
{
"input": {
"var": {
"remote_addr": "192.168.65.1",
"server_addr": "192.168.224.3",
"remote_port": "20939",
"server_port": "9080",
"timestamp": 1725260108
},
"request": {
"path": "/anything",
"port": 9080,
"method": "GET",
"scheme": "http",
"host": "127.0.0.1",
"query": {},
"headers": {
"host": "127.0.0.1:9080",
"user-agent": "curl/8.6.0",
"accept": "*/*"
}
},
"type": "http"
}
}
Now, update the plugin on the previously created route to include route information:
curl "http://127.0.0.1:9180/apisix/admin/routes/opa-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"plugins": {
"opa": {
"with_route": true
}
}
}'
Send a request to the route:
curl -i "http://127.0.0.1:9080/anything"
In the OPA server log, you should see a new entry:
{
"client_addr": "192.168.65.1:55642",
"level": "info",
"msg": "Received request.",
"req_body": "{\"input\":{\"route\":{\"create_time\":1725259073,\"update_time\":1725260366,\"priority\":0,\"status\":1,\"id\":\"opa-route\",\"plugins\":{\"opa\":{\"with_route\":true,\"with_service\":true,\"with_consumer\":false,\"keepalive\":true,\"keepalive_timeout\":60000,\"ssl_verify\":true,\"timeout\":3000,\"keepalive_pool\":5,\"host\":\"http://192.168.2.104:8181\",\"policy\":\"getonly\"}},\"uri\":\"/anything\"},\"request\":{\"path\":\"/anything\",\"port\":9080,\"method\":\"GET\",\"scheme\":\"http\",\"host\":\"127.0.0.1\",\"query\":{},\"headers\":{\"host\":\"127.0.0.1:9080\",\"user-agent\":\"curl/8.6.0\",\"accept\":\"*/*\"}},\"var\":{\"remote_addr\":\"192.168.65.1\",\"server_addr\":\"192.168.224.3\",\"remote_port\":\"23443\",\"server_port\":\"9080\",\"timestamp\":1725260373},\"type\":\"http\"}}",
"req_id": 4,
"req_method": "POST",
"req_params": {},
"req_path": "/v1/data/getonly",
"time": "2024-09-02T06:59:33Z"
}
The req_body
now includes route information:
{
"input": {
"route": {
"create_time": 1725259073,
"update_time": 1725260366,
"priority": 0,
"status": 1,
"id": "opa-route",
"plugins": {
"opa": {
"with_route": true,
"with_service": false,
"with_consumer": false,
"keepalive": true,
"keepalive_timeout": 60000,
"ssl_verify": true,
"timeout": 3000,
"keepalive_pool": 5,
"host": "http://192.168.2.104:8181",
"policy": "getonly"
}
},
"uri": "/anything"
},
"request": {
"path": "/anything",
"port": 9080,
"method": "GET",
"scheme": "http",
"host": "127.0.0.1",
"query": {},
"headers": {
"host": "127.0.0.1:9080",
"user-agent": "curl/8.6.0",
"accept": "*/*"
}
},
"var": {
"remote_addr": "192.168.65.1",
"server_addr": "192.168.224.3",
"remote_port": "23443",
"server_port": "9080",
"timestamp": 1725260373
},
"type": "http"
}
}
Return Custom Response
The following example demonstrates how you can return custom response code and message when the request is unauthorized.
Create an OPA policy that only allows HTTP GET requests and return 302
with a custom message the request is unauthorized:
curl "127.0.0.1:8181/v1/policies/customresp" -X PUT \
-H "Content-Type: text/plain" \
-d '
package customresp
import input.request
default allow = false
allow {
request.method == "GET"
}
# custom response body
# can be a string or an object
# the object will be returned to client in JSON format
reason = "The resource has temporarily moved. Please follow the new URL." {
not allow
}
# custom response header
headers = {
"Location": "http://example.com/auth"
} {
not allow
}
# custom response status code
status_code = 302 {
not allow
}'
Create a route with the opa
plugin:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "opa-route",
"uri": "/anything",
"plugins": {
"opa": {
"host": "http://192.168.2.104:8181",
"policy": "customresp"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
❶ Configure the OPA server address. Replace with your IP address.
❷ Set the authorization policy to be customresp
.
Send a GET request to the route:
curl -i "http://127.0.0.1:9080/anything"
You should receive an HTTP/1.1 200 OK
response.
Send a POST request to the route:
curl -i "http://127.0.0.1:9080/anything" -X POST
You should receive an HTTP/1.1 302 Moved Temporarily
response:
HTTP/1.1 302 Moved Temporarily
...
Location: http://example.com/auth
The resource has temporarily moved. Please follow the new URL.
Implement RBAC
The following example demonstrates how to implement authentication and RBAC using the jwt-auth
and opa
plugins. You will be implementing RBAC logics such that:
- An
user
role can only read the upstream resources. - An
admin
role can read and write the upstream resources.
Create an OPA policy for RBAC of two example consumers, where john
has the user
role and jane
has the admin
role:
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 {
t := input.request.headers.authorization
}
# Decode the token to get role and permission
token = {"payload": payload} {
[_, payload, _] := io.jwt.decode(bearer_token)
}
# Implement RBAC logic
default allow = false
allow {
# 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 permission granted to the role matches the user request
p == token.payload.permission
}'
Create two consumers john
and jane
in APISIX:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT -d '
{
"username": "john"
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT -d '
{
"username": "jane"
}'
Configure the jwt-auth
credentials for the consumers, using the default algorithm HS256
:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-key-auth",
"plugins": {
"jwt-auth": {
"key": "john-key",
"secret": "john-hs256-secret"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jane-key-auth",
"plugins": {
"jwt-auth": {
"key": "jane-key",
"secret": "jane-hs256-secret"
}
}
}'
Create a route and configure the jwt-auth
and opa
plugins as such:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "opa-route",
"methods": ["GET", "POST"],
"uris": ["/get","/post"],
"plugins": {
"jwt-auth": {},
"opa": {
"host": "http://192.168.2.104:8181",
"policy": "rbac"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org": 1
}
}
}'
❶ Enable the jwt-auth
plugin on the route.
❷ Configure the OPA server address. Replace with your IP address.
❸ Set the authorization policy to be rbac
.
Verify as john
To issue a JWT for john
, you could use JWT.io's debugger or other utilities. If you are using JWT.io's debugger, do the following:
- Select HS256 in the Algorithm dropdown.
- Update the secret in the Verify Signature section to be
john-hs256-secret
. - Update payload with role
user
, permissionread
, and consumer keyjohn-key
; as well asexp
ornbf
in UNIX timestamp.
If you are using API7 Enterprise, the requirement of exp
or nbf
is not mandatory. You can optionally include these claims and use the claims_to_verify
parameter to configure which claim to verify.
Your payload should look similar to the following:
{
"role": "user",
"permission": "read",
"key": "john-key",
"nbf": 1729132271
}
Copy the generated JWT under the Encoded section and save to a variable:
john_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInBlcm1pc3Npb24iOiJyZWFkIiwia2V5Ijoiam9obi1rZXkiLCJuYmYiOjE3MjkxMzIyNzF9.QuRWNeXtMg6GOTqrHKizH6o7eQj_zz3paC441-9Vv2g
Send a GET request to the route with the JWT of john
:
curl -i "http://127.0.0.1:9080/get" -H "Authorization: ${john_jwt_token}"
You should receive an HTTP/1.1 200 OK
response.
Send a POST request to the route with the same JWT:
curl -i "http://127.0.0.1:9080/post" -X POST -H "Authorization: ${john_jwt_token}"
You should receive an HTTP/1.1 403 Forbidden
response.
Verify as jane
Similarly, to issue a JWT for jane
, you could use JWT.io's debugger or other utilities. If you are using JWT.io's debugger, do the following:
- Select HS256 in the Algorithm dropdown.
- Update the secret in the Verify Signature section to be
jane-hs256-secret
. - Update payload with role
admin
, permission["read","write"]
, and consumer keyjane-key
; as well asexp
ornbf
in UNIX timestamp.
If you are using API7 Enterprise, the requirement of exp
or nbf
is not mandatory. You can optionally include these claims and use the claims_to_verify
parameter to configure which claim to verify.
Your payload should look similar to the following:
{
"role": "admin",
"permission": ["read","write"],
"key": "jane-key",
"nbf": 1729132271
}
Copy the generated JWT under the Encoded section and save to a variable:
jane_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjpbInJlYWQiLCJ3cml0ZSJdLCJrZXkiOiJqYW5lLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UpXEL0zfB6ciVITlDlzq41xVvCCfGOXvix284GcNKg8
Send a GET request to the route with the JWT of jane
:
curl -i "http://127.0.0.1:9080/get" -H "Authorization: ${jane_jwt_token}"
You should receive an HTTP/1.1 200 OK
response.
Send a POST request to the route with the same JWT:
curl -i "http://127.0.0.1:9080/post" -X POST -H "Authorization: ${jane_jwt_token}"
You should also receive an HTTP/1.1 200 OK
response.
To examine whether the authorization decision comes from OPA, you should observe the following log in the OPA server if you have set --log-level debug
:
{
"result":{
"allow": true,
"bearer_token": "eyJ...",
...
}
}