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 -sstarts OPA as a server.--log-level debugprints 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
userrole can only read the upstream resources. - An
adminrole 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 asexpornbfin 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 asexpornbfin 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...",
...
}
}