OPA
The opa plugin integrates APISIX and API7 Gateway with Open Policy Agent (OPA), a policy engine for defining and enforcing authorization decisions. Authorization logic is defined in Rego and stored in OPA.
For each request to a protected route, OPA evaluates the configured policy and tells the gateway whether to allow access to the upstream.
Examples
The examples show how to enforce request-method policies, inspect the data sent to OPA, return custom responses, and implement role-based access control.
Before proceeding, you should have a running OPA server. Start one using Docker or deploy it to Kubernetes:
- Docker
- Kubernetes
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-opa-net
docker network connect gateway-opa-net "$GATEWAY_CONTAINER"
Start OPA on the same network and publish its API only on the local host:
docker run -d \
--name opa-server \
--network gateway-opa-net \
-p 127.0.0.1:8181:8181 \
openpolicyagent/opa:1.20.2 \
run --server --addr=0.0.0.0:8181 --log-level=debug
Wait for OPA to become ready:
until curl --max-time 2 -fsS "http://127.0.0.1:8181/health" > /dev/null; do
sleep 1
done
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.20.2
args:
- run
- --server
- --addr=0.0.0.0:8181
- --log-level=debug
ports:
- name: http
containerPort: 8181
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: opa
spec:
selector:
app: opa
ports:
- name: http
port: 8181
targetPort: http
Apply the configuration to your cluster:
kubectl apply -f opa-server.yaml
kubectl rollout status -n aic deployment/opa
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:
- 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://opa-server:8181",
"policy": "getonly"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
❶ Configure the OPA server address reachable from the gateway.
❷ Set the authorization policy to be getonly.
services:
- name: opa-service
labels:
docs-example: opa
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://opa-server:8181"
policy: getonly
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ Configure the OPA server address reachable from the gateway.
❷ Set the authorization policy to be getonly.
Preview the changes to services with the example label:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
ports:
- name: http
port: 80
targetPort: 80
---
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
APISIX sends OPA an input document containing request context and any optional gateway resources enabled in the plugin configuration. This example continues with the policy and route from Implement a Basic Policy.
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.
Inspect the OPA server log. You should see an entry similar to the following. Addresses, ports, timestamps, and request metadata vary by environment:
{
"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.7.1\"},\"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.7.1"
},
"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
labels:
docs-example: opa
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://opa-server:8181"
policy: getonly
with_route: true
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Preview the changes to services with the example label:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
- 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
ports:
- name: http
port: 80
targetPort: 80
---
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. Addresses, ports, timestamps, and request metadata vary by environment:
{
"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://opa-server: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.7.1\"},\"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://opa-server: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.7.1"
},
"query": {},
"port": 9080,
"scheme": "http",
"method": "GET"
}
}
}
Return Custom Response
The following example returns a custom status code, header, and message when OPA denies a request.
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://opa-server:8181",
"policy": "customresp"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
❶ Configure the OPA server address reachable from the gateway.
❷ Set the authorization policy to be customresp.
services:
- name: opa-service
labels:
docs-example: opa
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://opa-server:8181"
policy: customresp
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ Configure the OPA server address reachable from the gateway.
❷ Set the authorization policy to be customresp.
Preview the changes to services with the example label:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
ports:
- name: http
port: 80
targetPort: 80
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-customresp-plugin-config
spec:
plugins:
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: customresp
---
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-customresp-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
❶ Configure the OPA server address.
❷ Set the authorization policy to be customresp.
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: customresp
❶ Configure the OPA server address.
❷ Set the authorization policy to be customresp.
Apply the configuration to your cluster:
kubectl apply -f opa-ic.yaml
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 combines the jwt-auth and opa plugins to enforce role-based access control:
- 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 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 result := {"allow": false}
result := {"allow": true} 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:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/consumers" \
-X PUT \
-H "X-API-KEY: ${ADMIN_API_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_API_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_API_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_API_KEY}" \
-d '{
"id": "cred-jane-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "jane-key",
"secret": "jane-hs256-secret-that-is-very-long"
}
}
}'
consumers:
- username: john
labels:
docs-example: opa
credentials:
- name: cred-john-jwt-auth
type: jwt-auth
config:
key: john-key
secret: john-hs256-secret-that-is-very-long
- username: jane
labels:
docs-example: opa
credentials:
- name: cred-jane-jwt-auth
type: jwt-auth
config:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
Preview the consumer changes, including their nested credentials:
adc diff -f adc.yaml \
--include-resource-type consumer \
--label-selector docs-example=opa
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type consumer \
--label-selector docs-example=opa
- Gateway API
- APISIX CRD
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: john
spec:
gatewayRef:
name: apisix
credentials:
- type: jwt-auth
name: cred-john-jwt-auth
config:
key: john-key
secret: john-hs256-secret-that-is-very-long
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jane
spec:
gatewayRef:
name: apisix
credentials:
- type: jwt-auth
name: cred-jane-jwt-auth
config:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
Apply the configuration to your cluster:
kubectl apply -f opa-consumers-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: john
spec:
ingressClassName: apisix
authParameter:
jwtAuth:
value:
key: john-key
secret: john-hs256-secret-that-is-very-long
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: jane
spec:
ingressClassName: apisix
authParameter:
jwtAuth:
value:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
Apply the configuration to your cluster:
kubectl apply -f opa-consumers-ic.yaml
Create a route with the jwt-auth and opa plugins:
- 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",
"methods": ["GET", "POST"],
"uris": ["/get","/post"],
"plugins": {
"jwt-auth": {},
"opa": {
"host": "http://opa-server:8181",
"policy": "rbac/result",
"with_consumer": true
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
❶ Enable the jwt-auth plugin on the route.
❷ Configure the OPA server address reachable from the gateway.
❸ Set the authorization policy to be rbac/result.
❹ Set with_consumer to true to send consumer information.
Update adc.yaml to add the route with jwt-auth and opa plugins:
consumers:
- username: john
credentials:
- name: cred-john-jwt-auth
type: jwt-auth
config:
key: john-key
secret: john-hs256-secret-that-is-very-long
- username: jane
credentials:
- name: cred-jane-jwt-auth
type: jwt-auth
config:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
services:
- name: opa-service
labels:
docs-example: opa
routes:
- name: opa-route
uris:
- /get
- /post
methods:
- GET
- POST
plugins:
jwt-auth: {}
opa:
host: "http://opa-server:8181"
policy: rbac/result
with_consumer: true
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
❶ Enable the jwt-auth plugin on the route.
❷ Configure the OPA server address reachable from the gateway.
❸ Set the authorization policy to be rbac/result.
❹ Set with_consumer to true to send consumer information.
Preview the changes to services with the example label:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=opa
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
ports:
- name: http
port: 80
targetPort: 80
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-rbac-plugin-config
spec:
plugins:
- name: jwt-auth
config:
_meta:
disable: false
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: rbac/result
with_consumer: true
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-rbac-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
method: GET
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-rbac-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
- matches:
- path:
type: Exact
value: /post
method: POST
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-rbac-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
❶ Configure the OPA server address.
❷ Set the authorization policy to be rbac/result.
❸ Set with_consumer to true to send consumer information.
Apply the configuration to your cluster:
kubectl apply -f opa-route-ic.yaml
When using the Ingress Controller, APISIX prefixes consumer names with the Kubernetes namespace. For example, a consumer named john in the aic namespace becomes aic_john. Update the OPA RBAC policy to use the prefixed names:
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 := {
"aic_john": ["user"],
"aic_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 result := {"allow": false}
result := {"allow": true} 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
}
'
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-rbac-route
spec:
ingressClassName: apisix
http:
- name: get-route
match:
methods:
- GET
paths:
- /get
upstreams:
- name: httpbin-external-domain
plugins:
- name: jwt-auth
enable: true
config:
_meta:
disable: false
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: rbac/result
with_consumer: true
- name: post-route
match:
methods:
- POST
paths:
- /post
upstreams:
- name: httpbin-external-domain
plugins:
- name: jwt-auth
enable: true
config:
_meta:
disable: false
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: rbac/result
with_consumer: true
Apply the configuration to your cluster:
kubectl apply -f opa-route-ic.yaml
When using the Ingress Controller, APISIX prefixes consumer names with the Kubernetes namespace. For example, a consumer named john in the aic namespace becomes aic_john. Update the OPA RBAC policy to use the prefixed names:
curl "http://127.0.0.1:8181/v1/policies/rbac" -X PUT \
-H "Content-Type: text/plain" \
-d '
package rbac
user_roles := {
"aic_john": ["user"],
"aic_jane": ["admin"]
}
permission_methods := {
"read": "GET",
"write": "POST"
}
role_permissions := {
"user": ["read"],
"admin": ["read", "write"]
}
bearer_token := t if {
t := input.request.headers.authorization
}
token := {"payload": payload} if {
[_, payload, _] := io.jwt.decode(bearer_token)
}
normalized_permissions := ps if {
ps := token.payload.permission
not is_string(ps)
}
normalized_permissions := [ps] if {
ps := token.payload.permission
is_string(ps)
}
default result := {"allow": false}
result := {"allow": true} if {
roles := user_roles[input.consumer.username]
r := roles[_]
permissions := role_permissions[r]
p := permissions[_]
permission_methods[p] == input.request.method
p in normalized_permissions
}
'
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
HS256as the algorithm. - Update the secret in the Valid secret section to be
john-hs256-secret-that-is-very-long. - Update payload with role
user, permissionread, and consumer keyjohn-key; as well asexpornbfin UNIX timestamp.
When claims_to_verify is a nonempty list, every listed claim is required and validated. When it is unset or empty, exp and nbf are validated when present, but neither claim is required.
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: ${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 JWT encoder or other utilities. If you are using JWT.io's JWT encoder, do the following:
- Fill in
HS256as 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 keyjane-key; as well asexpornbfin UNIX timestamp.
When claims_to_verify is a nonempty list, every listed claim is required and validated. When it is unset or empty, exp and nbf are validated when present, but neither claim is required.
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: ${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...",
...
}
}