OPA
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.
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. Start one using Docker or deploy it to Kubernetes:
- Docker
- Kubernetes
docker run -d --name opa-server -p 8181:8181 openpolicyagent/opa:1.6.0 run --server --addr :8181 --log-level debug
run -sstarts OPA as a server.--log-level debugprints debug information to examine the data APISIX pushes to OPA.
To verify that the OPA server is installed and 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
Create a Deployment and Service for OPA in your cluster:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: opa
spec:
replicas: 1
selector:
matchLabels:
app: opa
template:
metadata:
labels:
app: opa
spec:
containers:
- name: opa
image: openpolicyagent/opa:1.6.0
args:
- run
- --server
- --addr=:8181
- --log-level=debug
ports:
- containerPort: 8181
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: opa
spec:
selector:
app: opa
ports:
- port: 8181
targetPort: 8181
Apply the configuration to your cluster:
kubectl apply -f opa-server.yaml
Wait for the OPA pod to be ready. Once ready, the OPA server will be available within the cluster at http://opa.aic.svc.cluster.local:8181. To push policies to it from outside the cluster, set up a port-forward:
kubectl port-forward -n aic svc/opa 8181:8181 &
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 as such:
- Admin API
- ADC
- Ingress Controller
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.
services:
- name: opa-service
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://192.168.2.104:8181"
policy: getonly
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ Configure the OPA server address. Replace with your IP address.
❷ Set the authorization policy to be getonly.
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-plugin-config
spec:
plugins:
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: PathPrefix
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
❶ Configure the OPA server address.
❷ Set the authorization policy to be getonly.
Apply the configuration to your cluster:
kubectl apply -f opa-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: opa-route
spec:
ingressClassName: apisix
http:
- name: opa-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
❶ Configure the OPA server address.
❷ Set the authorization policy to be getonly.
Apply the configuration to your cluster:
kubectl apply -f opa-ic.yaml
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.215.1:58467",
"level": "info",
"msg": "Received request.",
"req_body": "{\"input\":{\"type\":\"http\",\"var\":{\"server_port\":\"9080\",\"timestamp\":1752400020,\"server_addr\":\"192.168.107.3\",\"remote_port\":\"58544\",\"remote_addr\":\"192.168.107.1\"},\"request\":{\"host\":\"127.0.0.1\",\"path\":\"/anything\",\"headers\":{\"host\":\"127.0.0.1:9080\",\"accept\":\"*/*\",\"user-agent\":\"curl/8.6.0\"},\"query\":{},\"port\":9080,\"scheme\":\"http\",\"method\":\"PUT\"}}}",
"req_id": 12,
"req_method": "POST",
"req_params": {},
"req_path": "/v1/data/getonly",
"time": "2025-07-14T15:07:00Z"
}
where the req_body shows the data APISIX pushed:
{
"input": {
"type": "http",
"var": {
"server_port": "9080",
"timestamp": 1752400020,
"server_addr": "192.168.107.3",
"remote_port": "58544",
"remote_addr": "192.168.107.1"
},
"request": {
"host": "127.0.0.1",
"path": "/anything",
"headers": {
"host": "127.0.0.1:9080",
"accept": "*/*",
"user-agent": "curl/8.6.0"
},
"query": {},
"port": 9080,
"scheme": "http",
"method": "PUT"
}
}
}
Now, update the plugin on the previously created route to include route information:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/opa-route" -X PATCH \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"plugins": {
"opa": {
"with_route": true
}
}
}'
Update adc.yaml to add with_route: true:
services:
- name: opa-service
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://192.168.2.104:8181"
policy: getonly
with_route: true
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Update opa-ic.yaml to add with_route: true:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-plugin-config
spec:
plugins:
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
with_route: true
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: PathPrefix
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Apply the updated configuration to your cluster:
kubectl apply -f opa-ic.yaml
Update opa-ic.yaml to add with_route: true:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: opa-route
spec:
ingressClassName: apisix
http:
- name: opa-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
with_route: true
Apply the updated configuration to your cluster:
kubectl apply -f opa-ic.yaml
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.215.1:43706",
"level": "info",
"msg": "Received request.",
"req_body": "{\"input\":{\"route\":{\"id\":\"opa-route\",\"uri\":\"/anything\",\"update_time\":1752395758,\"plugins\":{\"opa\":{\"keepalive_pool\":5,\"keepalive_timeout\":60000,\"host\":\"http://172.17.1.196:8181\",\"ssl_verify\":true,\"with_route\":true,\"with_service\":false,\"with_consumer\":false,\"timeout\":3000,\"keepalive\":true,\"policy\":\"getonly\"}},\"priority\":0,\"status\":1,\"create_time\":1752393063},\"type\":\"http\",\"var\":{\"server_port\":\"9080\",\"timestamp\":1752396233,\"server_addr\":\"192.168.107.3\",\"remote_port\":\"47838\",\"remote_addr\":\"192.168.107.1\"},\"request\":{\"host\":\"127.0.0.1\",\"path\":\"/anything\",\"headers\":{\"host\":\"127.0.0.1:9080\",\"accept\":\"*/*\",\"user-agent\":\"curl/8.6.0\"},\"query\":{},\"port\":9080,\"scheme\":\"http\",\"method\":\"GET\"}}}",
"req_id": 14,
"req_method": "POST",
"req_params": {},
"req_path": "/v1/data/getonly",
"time": "2025-07-13T08:43:53Z"
}
The req_body now includes route information:
{
"input": {
"route": {
"id": "opa-route",
"uri": "/anything",
"update_time": 1752395758,
"plugins": {
"opa": {
"keepalive_pool": 5,
"keepalive_timeout": 60000,
"host": "http://172.17.1.196:8181",
"ssl_verify": true,
"with_route": true,
"with_service": false,
"with_consumer": false,
"timeout": 3000,
"keepalive": true,
"policy": "getonly"
}
},
"priority": 0,
"status": 1,
"create_time": 1752393063
},
"type": "http",
"var": {
"server_port": "9080",
"timestamp": 1752396233,
"server_addr": "192.168.107.3",
"remote_port": "47838",
"remote_addr": "192.168.107.1"
},
"request": {
"host": "127.0.0.1",
"path": "/anything",
"headers": {
"host": "127.0.0.1:9080",
"accept": "*/*",
"user-agent": "curl/8.6.0"
},
"query": {},
"port": 9080,
"scheme": "http",
"method": "GET"
}
}
}
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 "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:
- Admin API
- ADC
- Ingress Controller
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
}
}
}'