title: Open Policy Agent (opa) keywords:
import Tabs from ‘@theme/Tabs’; import TabItem from ‘@theme/TabItem’;
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.
| Name | Type | Required | Default | Valid values | Description |
|---|---|---|---|---|---|
| host | string | True | Address of the OPA server. | ||
| policy | string | True | Policy 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_verify | boolean | False | true | If true, verify the OPA server's SSL certificate. | |
| timeout | integer | False | 3000 | [1, 60000] | Timeout for the HTTP call in milliseconds. |
| keepalive | boolean | False | true | If true, keep the connection alive for multiple requests. | |
| keepalive_timeout | integer | False | 60000 | >= 1000 | Idle time in milliseconds after which the connection is closed. |
| keepalive_pool | integer | False | 5 | >= 1 | The number of idle connections. |
| with_route | boolean | False | If true, send information of the current Route. | ||
| with_service | boolean | False | If true, send information of the current Service. | ||
| with_consumer | boolean | False | If 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_upstream | array[string] | False | >= 1 item | List of header names to forward from the OPA response to the Upstream service when the request is allowed. |
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.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.:::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:
<Tabs groupId=“opa-setup” defaultValue=“docker” values={[ {label: ‘Docker’, value: ‘docker’}, {label: ‘Kubernetes’, value: ‘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
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 &
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:
<Tabs groupId=“api” defaultValue=“admin-api” values={[ {label: ‘Admin API’, value: ‘admin-api’}, {label: ‘ADC’, value: ‘adc’}, {label: ‘Ingress Controller’, value: ‘aic’} ]}>
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.
services: - name: opa-service routes: - name: opa-route uris: - /anything plugins: opa: host: "http://127.0.0.1:8181" policy: getonly upstream: type: roundrobin nodes: - host: httpbin.org port: 80 weight: 1
Update host to your OPA server address. The policy is set to getonly.
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
<Tabs groupId=“k8s-api” defaultValue=“gateway-api” values={[ {label: ‘Gateway API’, value: ‘gateway-api’}, {label: ‘APISIX Ingress Controller’, value: ‘apisix-ingress-controller’} ]}>
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
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
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.
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:
<Tabs groupId=“api” defaultValue=“admin-api” values={[ {label: ‘Admin API’, value: ‘admin-api’}, {label: ‘ADC’, value: ‘adc’}, {label: ‘Ingress Controller’, value: ‘aic’} ]}>
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 } } }'
Update adc.yaml to add with_route: true:
services: - name: opa-service routes: - name: opa-route uris: - /anything plugins: opa: host: "http://127.0.0.1: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
<Tabs groupId=“k8s-api” defaultValue=“gateway-api” values={[ {label: ‘Gateway API’, value: ‘gateway-api’}, {label: ‘APISIX Ingress Controller’, value: ‘apisix-ingress-controller’} ]}>
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 (with --log-level debug), the req_body will now include Route information in addition to the request and var fields.
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:
<Tabs groupId=“api” defaultValue=“admin-api” values={[ {label: ‘Admin API’, value: ‘admin-api’}, {label: ‘ADC’, value: ‘adc’}, {label: ‘Ingress Controller’, value: ‘aic’} ]}>
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 } } }'
services: - name: opa-service routes: - name: opa-route uris: - /anything plugins: opa: host: "http://127.0.0.1:8181" policy: customresp upstream: type: roundrobin nodes: - host: httpbin.org port: 80 weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
<Tabs groupId=“k8s-api” defaultValue=“gateway-api” values={[ {label: ‘Gateway API’, value: ‘gateway-api’}, {label: ‘APISIX Ingress Controller’, value: ‘apisix-ingress-controller’} ]}>
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-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
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
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.
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:
user role can only read the Upstream resources.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:
<Tabs groupId=“api” defaultValue=“admin-api” values={[ {label: ‘Admin API’, value: ‘admin-api’}, {label: ‘ADC’, value: ‘adc’}, {label: ‘Ingress Controller’, value: ‘aic’} ]}>
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" } } }'
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
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
<Tabs groupId=“k8s-api” defaultValue=“gateway-api” values={[ {label: ‘Gateway API’, value: ‘gateway-api’}, {label: ‘APISIX Ingress Controller’, value: ‘apisix-ingress-controller’} ]}>
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
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 accordingly.
The ApisixConsumer CRD has a known issue where private_key is incorrectly required during the configuration. This issue will be addressed in a future release. At the moment, the example cannot be completed with APISIX CRDs.
Create a Route and configure the jwt-auth and opa Plugins:
<Tabs groupId=“api” defaultValue=“admin-api” values={[ {label: ‘Admin API’, value: ‘admin-api’}, {label: ‘ADC’, value: ‘adc’}, {label: ‘Ingress Controller’, value: ‘aic’} ]}>
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 } } }'
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 routes: - name: opa-route uris: - /get - /post methods: - GET - POST plugins: jwt-auth: {} opa: host: "http://127.0.0.1:8181" policy: rbac with_consumer: true upstream: type: roundrobin nodes: - host: httpbin.org port: 80 weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
<Tabs groupId=“k8s-api” defaultValue=“gateway-api” values={[ {label: ‘Gateway API’, value: ‘gateway-api’}, {label: ‘APISIX Ingress Controller’, value: ‘apisix-ingress-controller’} ]}>
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-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 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
Apply the configuration to your cluster:
kubectl apply -f opa-route-ic.yaml
The ApisixConsumer CRD has a known issue where private_key is incorrectly required during the configuration. This issue will be addressed in a future release. At the moment, the example cannot be completed with APISIX CRDs.
johnTo 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:
HS256 as the algorithm.john-hs256-secret-that-is-very-long.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.
janeSimilarly, 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:
HS256 as the algorithm.jane-hs256-secret-that-is-very-long.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.