| --- |
| title: Open Policy Agent (opa) |
| keywords: |
| - Apache APISIX |
| - API Gateway |
| - Plugin |
| - Open Policy Agent |
| - opa |
| description: The opa plugin integrates with Open Policy Agent, enabling unified policy definition and enforcement for authorization in API operations. |
| --- |
| |
| <!-- |
| # |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| --> |
| |
| <head> |
| <link rel="canonical" href="https://docs.api7.ai/hub/opa" /> |
| </head> |
| |
| import Tabs from '@theme/Tabs'; |
| import TabItem from '@theme/TabItem'; |
| |
| ## Description |
| |
| The `opa` Plugin supports the integration with [Open Policy Agent (OPA)](https://www.openpolicyagent.org), a unified policy engine and framework that helps define and enforce authorization policies. Authorization logic is defined in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) 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 |
| |
| | 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. | |
| |
| ## Data Definition |
| |
| ### APISIX to OPA Service |
| |
| The JSON below shows the data sent to the OPA service by APISIX: |
| |
| ```json |
| { |
| "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: |
| |
| ```json |
| { |
| "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: |
| |
| ```bash |
| 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'} |
| ]}> |
| |
| <TabItem value="docker"> |
| |
| ```shell |
| 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: |
| |
| ```shell |
| curl http://127.0.0.1:8181 | grep Version |
| ``` |
| |
| You should see a response similar to the following: |
| |
| ```text |
| Version: 1.6.0 |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="kubernetes"> |
| |
| Create a Deployment and Service for OPA in your cluster: |
| |
| ```yaml title="opa-server.yaml" |
| 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: |
| |
| ```shell |
| 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: |
| |
| ```shell |
| kubectl port-forward -n aic svc/opa 8181:8181 & |
| ``` |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| ### 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: |
| |
| ```shell |
| 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'} |
| ]}> |
| |
| <TabItem value="admin-api"> |
| |
| ```shell |
| 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`. |
| |
| </TabItem> |
| |
| <TabItem value="adc"> |
| |
| ```yaml title="adc.yaml" |
| 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: |
| |
| ```shell |
| adc sync -f adc.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="aic"> |
| |
| <Tabs |
| groupId="k8s-api" |
| defaultValue="gateway-api" |
| values={[ |
| {label: 'Gateway API', value: 'gateway-api'}, |
| {label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} |
| ]}> |
| |
| <TabItem value="gateway-api"> |
| |
| ```yaml title="opa-ic.yaml" |
| 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: |
| |
| ```shell |
| kubectl apply -f opa-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="apisix-ingress-controller"> |
| |
| ```yaml title="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: |
| |
| ```shell |
| kubectl apply -f opa-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| To verify the policy, send a GET request to the Route: |
| |
| ```shell |
| 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: |
| |
| ```shell |
| 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](#implement-a-basic-policy). |
| |
| 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'} |
| ]}> |
| |
| <TabItem value="admin-api"> |
| |
| ```shell |
| 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 |
| } |
| } |
| }' |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="adc"> |
| |
| Update `adc.yaml` to add `with_route: true`: |
| |
| ```yaml title="adc.yaml" |
| 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: |
| |
| ```shell |
| adc sync -f adc.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="aic"> |
| |
| <Tabs |
| groupId="k8s-api" |
| defaultValue="gateway-api" |
| values={[ |
| {label: 'Gateway API', value: 'gateway-api'}, |
| {label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} |
| ]}> |
| |
| <TabItem value="gateway-api"> |
| |
| Update `opa-ic.yaml` to add `with_route: true`: |
| |
| ```yaml title="opa-ic.yaml" |
| 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: |
| |
| ```shell |
| kubectl apply -f opa-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="apisix-ingress-controller"> |
| |
| Update `opa-ic.yaml` to add `with_route: true`: |
| |
| ```yaml title="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 |
| with_route: true |
| ``` |
| |
| Apply the updated configuration to your cluster: |
| |
| ```shell |
| kubectl apply -f opa-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| Send a request to the Route: |
| |
| ```shell |
| 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: |
| |
| ```shell |
| 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'} |
| ]}> |
| |
| <TabItem value="admin-api"> |
| |
| ```shell |
| 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 |
| } |
| } |
| }' |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="adc"> |
| |
| ```yaml title="adc.yaml" |
| 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: |
| |
| ```shell |
| adc sync -f adc.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="aic"> |
| |
| <Tabs |
| groupId="k8s-api" |
| defaultValue="gateway-api" |
| values={[ |
| {label: 'Gateway API', value: 'gateway-api'}, |
| {label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} |
| ]}> |
| |
| <TabItem value="gateway-api"> |
| |
| ```yaml title="opa-ic.yaml" |
| 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: |
| |
| ```shell |
| kubectl apply -f opa-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="apisix-ingress-controller"> |
| |
| ```yaml title="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: |
| |
| ```shell |
| kubectl apply -f opa-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| Send a GET request to the Route: |
| |
| ```shell |
| 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: |
| |
| ```shell |
| curl -i "http://127.0.0.1:9080/anything" -X POST |
| ``` |
| |
| You should receive an `HTTP/1.1 302 Moved Temporarily` response: |
| |
| ```text |
| 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: |
| |
| ```shell |
| 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'} |
| ]}> |
| |
| <TabItem value="admin-api"> |
| |
| ```shell |
| 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" |
| }' |
| ``` |
| |
| ```shell |
| 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`: |
| |
| ```shell |
| 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" |
| } |
| } |
| }' |
| ``` |
| |
| ```shell |
| 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" |
| } |
| } |
| }' |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="adc"> |
| |
| ```yaml title="adc.yaml" |
| 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: |
| |
| ```shell |
| adc sync -f adc.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="aic"> |
| |
| <Tabs |
| groupId="k8s-api" |
| defaultValue="gateway-api" |
| values={[ |
| {label: 'Gateway API', value: 'gateway-api'}, |
| {label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} |
| ]}> |
| |
| <TabItem value="gateway-api"> |
| |
| ```yaml title="opa-consumers-ic.yaml" |
| 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: |
| |
| ```shell |
| 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. |
| |
| </TabItem> |
| |
| <TabItem value="apisix-ingress-controller"> |
| |
| 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. |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| 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'} |
| ]}> |
| |
| <TabItem value="admin-api"> |
| |
| ```shell |
| 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 |
| } |
| } |
| }' |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="adc"> |
| |
| Update `adc.yaml` to add the Route with `jwt-auth` and `opa` Plugins: |
| |
| ```yaml title="adc.yaml" |
| 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: |
| |
| ```shell |
| adc sync -f adc.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="aic"> |
| |
| <Tabs |
| groupId="k8s-api" |
| defaultValue="gateway-api" |
| values={[ |
| {label: 'Gateway API', value: 'gateway-api'}, |
| {label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} |
| ]}> |
| |
| <TabItem value="gateway-api"> |
| |
| ```yaml title="opa-route-ic.yaml" |
| 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: |
| |
| ```shell |
| kubectl apply -f opa-route-ic.yaml |
| ``` |
| |
| </TabItem> |
| |
| <TabItem value="apisix-ingress-controller"> |
| |
| 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. |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| </TabItem> |
| |
| </Tabs> |
| |
| #### Verify as `john` |
| |
| To issue a JWT for `john`, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), 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: |
| |
| ```json |
| { |
| "role": "user", |
| "permission": "read", |
| "key": "john-key", |
| "nbf": 1729132271 |
| } |
| ``` |
| |
| Copy the generated JWT and save to a variable: |
| |
| ```text |
| export john_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInBlcm1pc3Npb24iOiJyZWFkIiwia2V5Ijoiam9obi1rZXkiLCJuYmYiOjE3MjkxMzIyNzF9.rAHMTQfnnGFnKYc3am_lpE9pZ9E8EaOT_NBQ5Ss8pk4 |
| ``` |
| |
| Send a GET request to the Route with the JWT of `john`: |
| |
| ```shell |
| 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: |
| |
| ```shell |
| 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](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), 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: |
| |
| ```json |
| { |
| "role": "admin", |
| "permission": ["read","write"], |
| "key": "jane-key", |
| "nbf": 1729132271 |
| } |
| ``` |
| |
| Copy the generated JWT and save to a variable: |
| |
| ```text |
| export jane_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjpbInJlYWQiLCJ3cml0ZSJdLCJrZXkiOiJqYW5lLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.meZ-AaGHUPwN_GvVOE3IkKuAJ1wqlCguaXf3gm3Ww8s |
| ``` |
| |
| Send a GET request to the Route with the JWT of `jane`: |
| |
| ```shell |
| 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: |
| |
| ```shell |
| 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. |