Skip to main content
Version: 3.17

Open Policy Agent (opa)

Description#

The opa Plugin supports the integration with Open Policy Agent (OPA), a unified policy engine and framework that helps define and enforce authorization policies. Authorization logic is 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.

Attributes#

NameTypeRequiredDefaultValid valuesDescription
hoststringTrueAddress of the OPA server.
policystringTruePolicy path to evaluate. The plugin appends this value to /v1/data/<policy>, so it must point to an OPA document/decision whose result is an object containing an allow field. The returned object can also include reason, headers, and status_code. For example, use a path such as rbac only if querying /v1/data/rbac returns an object with this structure, rather than a bare boolean result.
ssl_verifybooleanFalsetrueIf true, verify the OPA server's SSL certificate.
timeoutintegerFalse3000[1, 60000]Timeout for the HTTP call in milliseconds.
keepalivebooleanFalsetrueIf true, keep the connection alive for multiple requests.
keepalive_timeoutintegerFalse60000>= 1000Idle time in milliseconds after which the connection is closed.
keepalive_poolintegerFalse5>= 1The number of idle connections.
with_routebooleanFalseIf true, send information of the current Route.
with_servicebooleanFalseIf true, send information of the current Service.
with_consumerbooleanFalseIf true, send information of the current Consumer. Note that the Consumer information may include sensitive information such as the API key. Only set this option to true if you are sure it is safe to do so.
send_headers_upstreamarray[string]False>= 1 itemList of header names to forward from the OPA response to the Upstream service when the request is allowed.

Data Definition#

APISIX to OPA Service#

The JSON below shows the data sent to the OPA service by APISIX:

{
"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": {}
}

Each of these keys are explained below:

  • type indicates the request type (http or stream).
  • request is used when the type is http and contains the basic request information (URL, headers etc).
  • var contains the basic information about the requested connection (IP, port, request timestamp etc).
  • route, service and consumer contains the same data as stored in APISIX and are only sent if the opa Plugin is configured on these objects.

OPA Service to APISIX#

The JSON below shows the response from the OPA service to APISIX:

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

The keys in the response are explained below:

  • allow is indispensable and indicates whether the request is allowed to be forwarded through APISIX.
  • reason, headers, and status_code are optional and are only returned when you configure a custom response.

Examples#

note

You can fetch the admin_key from conf/config.yaml and save to an environment variable with the following command:

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

Before proceeding, you should have a running OPA server. Start one using Docker or deploy it to Kubernetes:

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

To verify that the OPA server is installed and the port is exposed properly, run:

curl http://127.0.0.1:8181 | grep Version

You should see a response similar to the following:

Version: 1.6.0

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

default allow = false

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

Create a Route with the opa Plugin:

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
}
}
}'

Update host to your OPA server address. The policy is set to 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.

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 PATCH \
-H "X-API-KEY: ${admin_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 (with --log-level debug), the req_body will now include Route information in addition to the request and var fields.

Return Custom Response#

The following example demonstrates how you can return a custom response code and message when the request is unauthorized.

Create an OPA policy that only allows HTTP GET requests and returns 302 with a custom message when the request is unauthorized:

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
}
'

Create a Route with the opa Plugin:

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
}
}
}'

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 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
}
'

Create two Consumers john and jane in APISIX and configure their jwt-auth Credentials:

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"
}'

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_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"
}
}
}'

Create a Route and configure the jwt-auth and opa Plugins:

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
}
}
}'

Verify as john#

To issue a JWT for john, you could use JWT.io's JWT encoder or other utilities. If you are using JWT.io's JWT encoder, do the following:

  • Fill in HS256 as the algorithm.
  • Update the secret in the Valid secret section to be john-hs256-secret-that-is-very-long.
  • Update payload with role user, permission read, and Consumer key john-key; as well as exp or nbf in UNIX timestamp.

Your payload should look similar to the following:

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

Copy the generated JWT and save to a variable:

export john_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInBlcm1pc3Npb24iOiJyZWFkIiwia2V5Ijoiam9obi1rZXkiLCJuYmYiOjE3MjkxMzIyNzF9.rAHMTQfnnGFnKYc3am_lpE9pZ9E8EaOT_NBQ5Ss8pk4

Send a GET request to the Route with the JWT of john:

curl -i "http://127.0.0.1:9080/get" -H "Authorization: Bearer ${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: Bearer ${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 JWT encoder or other utilities. If you are using JWT.io's JWT encoder, do the following:

  • Fill in HS256 as the algorithm.
  • Update the secret in the Valid secret section to be jane-hs256-secret-that-is-very-long.
  • Update payload with role admin, permission ["read","write"], and Consumer key jane-key; as well as exp or nbf in UNIX timestamp.

Your payload should look similar to the following:

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

Copy the generated JWT and save to a variable:

export jane_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjpbInJlYWQiLCJ3cml0ZSJdLCJrZXkiOiJqYW5lLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.meZ-AaGHUPwN_GvVOE3IkKuAJ1wqlCguaXf3gm3Ww8s

Send a GET request to the Route with the JWT of jane:

curl -i "http://127.0.0.1:9080/get" -H "Authorization: Bearer ${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: Bearer ${jane_jwt_token}"

You should also receive an HTTP/1.1 200 OK response.