RBAC with API Gateway and Open Policy Agent(OPA)
Tags: webdev, rest, security, rbac
Published: 2023-05-16 07:59:43

With various access control models and implementation methods available, constructing an authorization system for backend service APIs can still be challenging. However, the ultimate goal is to ensure that the correct individual has appropriate access to the relevant resource. In this article, we will discuss **how to enable the Role-based access control(RBAC)** authorization model for your API with open-source API Gateway [Apache APISIX](https://apisix.apache.org/) and [Open Policy Agent](https://www.openpolicyagent.org/docs/latest/) (OPA).

## Learning objectives

You will learn the following throughout the article:

- What is RBAC and how it works?
- What is OPA and how it works?
- How to implement RBAC with OPA and Apache APISIX?
- How to define and register the policy in OPA.
- How to create an upstream, route, and enable OPA plugin.
- How the user’s role and permission is parsed from JWT token payload or consumer data.

## What is RBAC?

[Role-Based Access Control](https://en.wikipedia.org/wiki/Role-based_access_control) (RBAC)and [Attribute-Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) (ABAC) are two commonly used access control models used to **manage permissions and control access to resources** in computer systems.  RBAC assigns permissions to users based on their role within an organization. In RBAC, roles are defined based on the functions or responsibilities of users, and permissions are assigned to those roles. Users are then assigned to one or more roles, and they inherit the permissions associated with those roles. In the API context, for example, a developer role might have permission to create and update API resources, while an end-user role might only have permission to read or execute API resources.  

Basically, RBAC assigns permissions **based on user roles**, while ABAC assigns permissions **based on attributes** associated with users and resources.

> In RBAC, a policy is defined by the combination of a user's assigned role, the actions they are authorized to perform, and the resources on which they can perform those actions.

## What is OPA?

[OPA (Open Policy Agent)](https://www.openpolicyagent.org/docs/latest/) is a policy engine and a set of tools that provide a unified approach to policy enforcement across an entire distributed system. It allows you to define, manage, and enforce policies centrally from a single point. By defining policies as code, OPA enables easy review, editing, and roll-back of policies, facilitating efficient policy management.

![Open Policy Agent example image](https://static.apiseven.com/uploads/2023/05/13/KIm9OQHJ_Open%20Policy%20Agent%20Example%C2%A0.jpg)

OPA provides **a declarative language** called [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), which allows you to create and enforce policies throughout your stack. When you request a policy decision from OPA, it uses the rules and data that you have provided in a `.rego` file to evaluate the query and produce a response. The query result is then sent back to you as the policy decision. OPA stores all the policies and the data it needs in its in-memory cache. As a result, OPA returns results quickly. Here is an example of a simple OPA Rego file:

```sql
package example

default allow = false

allow {
    input.method == "GET"
    input.path =="/api/resource"
    input.user.role == "admin"
}
```

In this example, we have a package called "example" that defines a rule called "allow”. The "allow" rule specifies that the request is allowed if the input method is "GET", the requested path is `/api/resource`, and the user's role is "admin". If these conditions are met, then the "allow" rule will evaluate as "true", allowing the request to proceed.

## Why use OPA and API Gateway for RBAC?

API Gateway provides a centralized location to configure and manage API, and API consumers. It can be used as a [centralized authentication gateway](https://medium.com/@bumurzaqov2/centralized-authentication-with-apache-apisix-plugins-241b82865c00) by **avoiding having each individual service implement authentication logic** inside the service itself. On the other hand, OPA adds an authorization layer and decouples the policy from the code by creating a distinct benefit for authorization.  With this combination, you can add permissions for an API resource to a role. Users might be associated with one or more user roles. Each user role defines a set of permissions (GET, PUT, DELETE) on RBAC resources (defined by URI paths). In the next section, let’s learn how to achieve RBAC using these two.

![Open Policy Agent with Apache APISIX API Gateway](https://static.apiseven.com/uploads/2023/05/13/fgnPdpBS_opa-with-api-gateway.png)

## How to implement RBAC with OPA and Apache APISIX

In Apache APISIX, you can configure [routes](https://apisix.apache.org/docs/apisix/terminology/route/) and [plugins](https://apisix.apache.org/docs/apisix/terminology/plugin/) to define the behavior of your API. You can use the APISIX [opa plugin](https://apisix.apache.org/docs/apisix/plugins/opa/) to enforce RBAC policies by forwarding requests to OPA for decision-making. Then OPA makes an authorization decision based on users’ roles and permissions in real-time.

Assume that we have [Conference API](https://conferenceapi.azurewebsites.net/) where you can retrieve/edit event sessions, topics, and speaker information. A speaker can only read their own sessions and topics while the admin can add/edit more sessions and topics. Or attendees can leave their feedback about the speaker's session via a POST request to `/speaker/speakerId/session/feedback` and the speaker can only see by requesting the GET method of the same URI.  The below diagram illustrates the whole scenario:

![Open Policy Agent with Apache APISIX](https://static.apiseven.com/uploads/2023/05/14/ExspMYZe_Role-Based%20Access%20Control%20%28RBAC%29%20with%20API%20Gateway%20and%20OPA%20%282%29.png)

1. API consumer requests a route on the API Gateway with its credential such as a JWT token in the [authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization).
2. API Gateway sends consumer data with a JWT header to the OPA engine.
3. OPA evaluates if the consumer has a right to access the resource by using policies (roles and permissions) we specify in the .rego file.
4. If the OPA decision is allowed, then the request will be forwarded to the upstream Conference service.

Next, we install, configure APISIX, and define policies in OPA.

### Prerequisites

- [Docker](https://docs.docker.com/get-docker/) is used to installing the containerized etcd and APISIX.
- [curl](https://curl.se/) is used to send requests to APISIX Admin API. You can also use tools such as [Postman](https://www.postman.com/) to interact with the API.

### Step 1: Install Apache APISIX

APISIX can be easily installed and started with the following quickstart script:

```bash
curl -sL https://run.api7.ai/apisix/quickstart | sh
```

### Step 2: Configure the backend service (upstream)

To route requests to the backend service for the Conference API, you'll need to configure it by adding an upstream server in Apache APISIX via the [Admin API](https://apisix.apache.org/docs/apisix/admin-api/).

```bash
curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -X PUT -d '
{
   "name":"Conferences API upstream",
   "desc":"Register Conferences API as the upstream",
   "type":"roundrobin",
   "scheme":"https",
   "nodes":{
      "conferenceapi.azurewebsites.net:443":1
   }
}'
```

### Step 3: Create an API consumer

Next, we create a consumer (a new speaker) with the username `jack` in Apache APISIX. It sets up the [jwt-auth](https://apisix.apache.org/docs/apisix/plugins/jwt-auth/) plugin for the consumer with the specified key and secret. This will allow the consumer to authenticate using a [JSON Web Token](https://jwt.io/) (JWT).

```bash
curl http://127.0.0.1:9180/apisix/admin/consumers -X PUT -d '
{
    "username": "jack",
    "plugins": {
        "jwt-auth": {
            "key": "user-key",
            "secret": "my-secret-key"
        }
    }
}'
```

### Step 4: Create a public endpoint to generate a JWT token

You also need to set up a new Route that generates and signs the token using the [public-api](https://apisix.apache.org/docs/apisix/plugins/public-api/) plugin. In this scenario, API Gateway acts as an identity provider server to create and verify the token with our consumer jack’s key. The identity provider can be also any other 3rd party services such as [Google](https://developers.google.com/identity), [Okta](https://www.okta.com/),  [Keycloak](https://www.keycloak.org/), and [Ory Hydra](https://www.ory.sh/hydra/).

```bash
curl http://127.0.0.1:9180/apisix/admin/routes/jas -X PUT -d '
{
    "uri": "/apisix/plugin/jwt/sign",
    "plugins": {
        "public-api": {}
    }
}'
```

### Step 5: Claim a new JWT token for the API consumer

Now we can get a new token for our speaker Jack from the API Gateway using the public endpoint we created. The following curl command generates a new token with Jack’s credentials and assigns role, and permission in the payload.

```bash
curl -G --data-urlencode 'payload={"role":"speaker","permission":"read"}' http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key -i
```

After you run the above command, you will receive a token as a response. Save this token somewhere, later we are going to use this token to access our new API Gateway endpoint.

### Step 6: Create a new plugin config

This step involves configuring APISIX’s 3 plugins, [proxy-rewrite](https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite/), [jwt-auth](https://apisix.apache.org/docs/apisix/plugins/jwt-auth/) and [opa](https://apisix.apache.org/docs/apisix/plugins/opa/) plugins.

```bash
curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d '
{
   "plugins":{
      "jwt-auth":{
      },
      "proxy-rewrite":{
         "host":"conferenceapi.azurewebsites.net"
      }
   }
}'
```

- The `proxy-rewrite` plugin is configured to proxy requests to the `conferenceapi.azurewebsites.net` host.
- OPA authentication plugin is configured to use the OPA policy engine running at [http://localhost:8181/v1/data/rbacExample](http://localhost:8181/v1/data/example). Also, APISIX **sends all consumer-related information to OPA**.  We will add this policy .`rego` file in the Opa configuration section.

### Step 7: Create a Route for Conference sessions

The final step is to create a new route for Conferences API speaker sessions:

```bash
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT -d '
{
    "name":"Conferences API speaker sessions route",
    "desc":"Create a new route in APISIX for the Conferences API speaker sessions",
    "methods": ["GET", "POST"],
    "uris": ["/speaker/*/topics","/speaker/*/sessions"],
    "upstream_id":"1",
    "plugin_config_id":1
}'
```

The payload contains information about the route, such as its name, description, methods, URIs, upstream ID, and plugin configuration ID. In this case, the route is configured to handle GET and POST requests for two different URIs, `/speaker/topics` *and* `/speaker/sessions`. The "upstream_id" field specifies the ID of the upstream service that will handle incoming requests for this route, while the "plugin_config_id" field specifies the ID of the plugin configuration to be used for this route.

### Step 8: Test the setup without OPA

So far, we have set up all the necessary configurations for APISIX to direct incoming requests to Conference API endpoints, only allowing authorized API consumers. Now, each time an API consumer wants to access an endpoint, they must provide a JWT token to retrieve data from the Conference backend service. You can verify this by hitting the endpoint and the domain address we are requesting now is our custom API Gateway but not an actual Conference service:

```bash
curl -i http://127.0.0.1:9080/speaker/1/topics -H 'Authorization: {API_CONSUMER_TOKEN}'
```

### Step 9: Run OPA service

The other two steps are we run the OPA service using Docker and upload our policy definition using its API which can be used to evaluate authorization policies for incoming requests.

```bash
docker run -d --network=apisix-quickstart-net --name opa -p 8181:8181 openpolicyagent/opa:latest run -s
```

This Docker command runs a container of the OPA image with the latest version. It creates a new container on the existing APISIX network `apisix-quickstart-net`with the name `opa`and exposes `port 8181`. So, APISIX can send policy check requests to OPA directly using the address `[http://opa:8181](http://opa:8181)` Note that OPA and APISIX should run in the same docker network.

### Step 10: Define and register the policy

The second step on the OPA side is you need to define the policies that will be used to control access to API resources. These policies should define the attributes required for access (Which **users** have which **roles**) and the permission (Which **roles** have which **permissions**) that are allowed or denied based on those attributes. For example, in the below configuration, we are saying to OPA check the `user_roles` table what is the role for `jack`. This information is sent by APISIX inside `input.consumer.username`. Also, we are verifying the consumer’s permission by reading the JWT payload and extracting `token.payload.permission` from there. The comments describe the steps clearly.

```bash
curl -X PUT '127.0.0.1:8181/v1/policies/rbacExample' \
    -H 'Content-Type: text/plain' \
    -d 'package rbacExample

# Assigning user roles

user_roles := {
    "jack": ["speaker"],
    "bobur":["admin"]
}

# Role permission assignments
role_permissions := {
    "speaker": [{"permission": "read"}],
    "admin":   [{"permission": "read"}, {"permission": "write"}]
}

# Helper JWT Functions
bearer_token := t {
 t := input.request.headers.authorization
}

# Decode the authorization token to get a role and permission
token = {"payload": payload} {
 [_, payload, _] := io.jwt.decode(bearer_token)
}

# Logic that implements RBAC
default allow = false

allow {
    # Lookup the list of roles for the user
    roles := user_roles[input.consumer.username]

    # For each role in that list
    r := roles[_]

    # Lookup the permissions list for role r
    permissions := role_permissions[r]

    # For each permission
    p := permissions[_]

    # Check if the permission granted to r matches the users request
    p == {"permission": token.payload.permission}
}'
```

### Step 11: Update the existing plugin config with the OPA plugin

Once we defined policies on the OPA service, we need to update the existing plugin config for the route to use the OPA plugin. We specify in the `policy` attribute of the OPA plugin.

```bash
curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PATCH -d '
{
   "plugins":{
      "opa":{
         "host":"http://opa:8181",
         "policy":"rbacExample",
         "with_consumer":true
      }
   }
}'
```

### Step 12: Test the setup with OPA

Now we can test all setups we did with OPA policies. If you try to run the same curl command to access the API Gateway endpoint, it first checks the JWT token as the authentication process and sends consumer and JWT token data to OPA to verify the role and permission as the authorization process. Any request without a JWT token in place or allowed roles will be denied.

```bash
curl -i http://127.0.0.1:9080/speaker/1/topics -H 'Authorization: {API_CONSUMER_TOKEN}'
```

## Conclusion

In this article, we learned how to implement RBAC with OPA and Apache APISIX. We defined a simple custom policy logic to allow/disallow API resource access based on our API consumer’s role and permissions. Also, this tutorial demonstrated how we can **extract API consumer-related information** in the policy file from the **JWT token payload** or **consumer object** sent by APISIX.

### Related resources

- [Make your security policy auditable](https://blog.frankel.ch/security-policy-auditable/)
- [Apache APISIX Authorization Policy: Protect Your APIs](https://api7.ai/blog/apisix-permission-policy)
- [Centralized Authentication with Apache APISIX Plugins](https://medium.com/@bumurzaqov2/centralized-authentication-with-apache-apisix-plugins-241b82865c00)
- [Manage API Consumers with Apache APISIX](https://medium.com/apache-apisix/manage-api-consumers-with-apache-apisix-b38245345814)

### Recommended content

- [Dynamic routing based on user credentials with API Gateway](https://api7.ai/blog/dynamic-routing-based-on-user-credentials)
- [Why Is Apache APISIX the Best API Gateway?](https://api7.ai/blog/why-is-apache-apisix-the-best-api-gateway)

### Community

🙋 [Join the Apache APISIX Community](https://apisix.apache.org/docs/general/join/)
🐦 [Follow us on Twitter](https://twitter.com/ApacheAPISIX)
📝 [Find us on Slack](https://join.slack.com/t/the-asf/shared_invite/zt-vlfbf7ch-HkbNHiU_uDlcH_RvaHv9gQ)
💁 [How to contribute page](https://apisix.apache.org/docs/general/how-to-contribute/)

### About the author

Visit my blog: [www.iambobur.com](https://www.iambobur.com/)