Skip to main content

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, permission read, and consumer key john-key; as well as exp or nbf in UNIX timestamp.
note

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 key jane-key; as well as exp or nbf in UNIX timestamp.
note

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.

tip

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...",
...
}
}

API7.ai Logo

API Management for Modern Architectures with Edge, API Gateway, Kubernetes, and Service Mesh.

Product

API7 Cloud

SOC2 Type IIISO 27001HIPAAGDPRRed Herring

Copyright © APISEVEN PTE. LTD 2019 – 2024. Apache, Apache APISIX, APISIX, and associated open source project names are trademarks of the

Apache Software Foundation