How to implement RBAC with API Gateway and OPA

2023.06.27

How to implement RBAC with API Gateway and OPA

 
In this article, you will learn how to enable role-based access control (RBAC) using the open source API Gateway Apache APISIX and Open Policy Agent (OPA).

Currently, in order to ensure that the right people can access the right resources, we need to enable appropriate access control methods on the system.  However, in the face of various well-known implementation models, building an API authorization system for its back- end services is often a big challenge.  In this article, we will discuss how to use the open source API Gateway - Apache APISIX ( https://apisix.apache.org/ ) and Open Policy Agent (Open Policy Agent, OPA, https ://www.openpolicyagent.org/docs/latest/ ) to enable a role-based access control (RBAC) authorization model for its own API.    

What is RBAC?

Role-based access control (RBAC, https://en.wikipedia.org/wiki/Role-based_access_control) and attribute-based access control (ABAC, https://en.wikipedia.org/wiki/Attribute-based_access_control) are two of the most commonly used access control models that can be used to manage permissions and access to resources in a computer system.  Usually, RBAC assigns permissions to users based on their roles, functions and responsibilities in the organization.

That is to say, in RBAC, roles are defined according to user's functions or responsibilities, and corresponding permissions are assigned to these roles.  Of course, in actual operation, we often assign one or more roles to users so that they can inherit the permission the s associated with these roles.  For example, in the context of an API, a developer role might have permissions to create and update API resources, while an end user role only has permissions to read or execute API resources. Also,  in RBAC, policies are defined by a combination of the roles users are assigned, the operations they are authorized to perform, and the resources they need to perform the operations. If RBAC assigns permissions based on user roles, then ABAC assigns permissions based on attributes associated with users and resources.

What are OPAs?

As a policy engine and a set of tools, OPA provides a unified approach to enforce policy across an entire distributed system. It  allows developers to centrally define, manage and enforce policies from a single endpoint.  By defining policies as code, OPA facilitates efficient policy management by making it easy to review, edit and roll back policies.

As shown above, OPA provides a declarative language called Rego (https://www.openpolicyagent.org/docs/latest/policy-language/).  It allows you to create and enforce various policies across your technology stack.  When you ask OPA for a policy decision, it uses the rules and data you provide in the file to evaluate the query and generate a response, and then sends the result of the query back to you as a policy decision. Because OPA stores all the policies and data  it needs in its internal cache, it returns results quickly.  Here is an example of a simple OPA Rego file:

package example

default allow = false
allow {
   
input.method == "GET"
   
input.path =="/api/resource"
    input.user.role == "admin"
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

As shown in the snippet above, we have a package called "example" that defines a rule called "allow".  This rule specifies: If the input method is "GET", the requested path is /api/resource, and the user role is "admin", then the request is allowed.  That is, if both of these conditions are met, the "allow" rule evaluates to "true", allowing the request to proceed.

Why use OPA and API Gateway for RBAC?

API Gateway provides a centralized location to configure and manage APIs and their consumers.  As a centralized authentication gateway, it effectively avoids having each individual service implement the authentication logic inside itself.  OPA, on the other hand, separates policy from code by creating a separate authorization layer for authorization.  With this combination, you can add API resource permissions to roles, and then define a set of permissions (GET, PUT, DELETE) for RBAC resources (defined by URI paths) for each user role. In the next  section , we will learn how to use both to implement RBAC.

How to implement RBAC using 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 the API.  Specifically, you can use APISIX's OPA plugin (https://apisix.apache.org/docs/apisix/plugins/opa/) to implement RBAC-related policies by forwarding requests to OPA for decision -making.  In other words, OPA will make authorization decisions in real time based on the user's roles and permissions.

Suppose we have a Conference API (https://conferenceapi.azurewebsites.net/) where you can retrieve/edit active sessions, topics, and speaker information.  In terms of authorization, speakers can only read their own sessions and topics, while administrators can add/edit more sessions and topics.  Also, attendees can leave their feedback for the speaker session via a POST request to the /speaker/speakerId/session/feedback path, which can only be seen by the speaker via a GET method requesting the same URI.  The image below shows the whole scene:

1. The API consumer will use its credentials (such as: JWT token in the authorization header, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) on the API gateway to request routing.

2. API gateway sends consumer data with JWT header to OPA engine.

3. OPA uses the policies we specified in the .rego file (such as: roles and permissions) to evaluate whether the user has the right to access resources.

4. If the OPA decision is "Allow", the request will be forwarded to the upstream Conference service.

Next, let's install and configure APISIX, and define various policies in OPA.

prerequisites

  • Docker (https://docs.docker.com/get-docker/), used to install containerized etcd and APISIX.
  • Curl (https://curl.se/), used to send requests to APISIX Admin API.  Of course, 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 quickly started using the following script:

curl -sL https://run.api7.ai/apisix/quickstart | sh
  • 1.

Step 2: Configure backend services (upstream)

In order to route requests to the backend service of the Conference API, you need to configure it by adding an upstream server in Apache APISIX via the Admin API (https://apisix.apache.org/docs/apisix/admin-api/ ).  Please see the following code:

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
   }
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

Step 3: Create an API consumer

Next, we create a consumer (ie, a new speaker) in  Apache  APISIX  with the username  Jack , and set up  jwt-auth for the consumer with the specified key  ( https://apisix.apache.org/docs/apisix/plugins /jwt-auth/ ) plugin so that consumers can use JSON Web Token ( JWT , https://jwt.io/ ) for authentication. Please see the following code:         

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"
        }
    }
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

Step 4: Create a public endpoint to generate JWT tokens

You also need to set up a new route that uses the  public-api  ( https://apisix.apache.org/docs/apisix/plugins/public-api/ ) plugin to generate and issue tokens. At this point, API Gateway will act as an identity provider server ( identity provider server ) to verify the token created and verified by the user Jack 's key. Of course, the identity provider can also be such as Google ( https://developers.google.com/ identity ), Okta (https://www.okta.com/), Keycloak ( https://www.keycloak.org/ ) and any third-party services such as Ory Hydra ( https://www.ory.sh /hydra/ ).                     Please see the following code:

curl http://127.0.0.1:9180/apisix/admin/routes/jas
-X PUT -d '
{
   
"uri": "/apisix/plugin/jwt/sign",
   
"plugins": {
        "public-api": {}
    }
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

Step 5: Request a new JWT token for the API consumer

We can now obtain a new token for Jack from the API Gateway using the public endpoint we have created.  The following curl command generates a new token using Jack's credentials and assigns roles and permissions in the payload.

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

After running the above command, you will receive a new token in response.  Let's save this token somewhere for now so we can use it later to access the new API Gateway endpoint.

Step 6: Create a new plugin configuration

This step will involve configuring 3 APISIX plugins, the distribution is: 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. Please see the following code:         

curl
"http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d '
{
  
"plugins":{
     
"jwt-auth":{
      },
     
"proxy-rewrite":{
        
"host":"conferenceapi.azurewebsites.net"
      }
   }
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • The proxy-rewrite plugin is configured to proxy all requests to the conferenceapi.azurewebsites.net host.
  • The OPA authentication plugin is configured to use  the OPA policy engine running at  http://localhost:8181/v1/data/rbacExample .  In addition, APISIX sends all user-related information to OPA.  We need to add the .rego file in the configuration section of OPA.

Step 7: Create Routes for Conference Sessions

The final step is to create a new route for the conferences API's speaker sessions:

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
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

The above payload contains routing information such as: name, description, method, URI, upstream ID, and plugin configuration ID.  In this example, the route is configured to handle GET and POST requests for two different URIs: /speaker/topics and / speaker/sessions .  Among them, the "upstream_id" field specifies the ID of the upstream service that will handle incoming requests for this route, and the "plugin_config_id" field specifies the ID of the plugin configuration that will 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 the various endpoints of the Conference API, and only allow those API consumers who have been authorized. According to this, every time an API consumer needs 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.  Here, the domain address we requested is the custom API gateway, not the actual conference service:

curl -i http://127.0.0.1:9080/speaker/1/topics -H
'Authorization: {API_CONSUMER_TOKEN}'
  • 1.
  • 2.

Step 9: Run the OPA service

Next, we use Docker to run the OPA service and use its API to upload our policy definitions.  This API can be used to evaluate authorization policies for individual incoming requests.

docker run -d --network=apisix-quickstart-net
--name opa -p 8181:8181 openpolicyagent/opa:latest run -s
  • 1.
  • 2.

The above Docker command starts an OPA image container with the latest version.  It creates a new container named OPA on the existing APISIX network apisix-quickstart-net and exposes port 8181.  Therefore, APISIX can directly use the address  --[http:/ /opa:8181](  http://opa:8181  )  to send a policy check request to OPA.  Note that OPA and APISIX should run in the same Docker network.

Step 10: Define and Register Policies

The next step on the OPA side is the need to define the policies that will be used to control access to API resources.  These policies should define the attributes required for access (eg, which users have which roles), and the permissions allowed or denied based on those attributes (eg, which roles have which permissions).  For example, in the configuration below we ask OPA to check the table user_roles to find the role Jack.  These messages are sent by input.consumer.username inside APISIX.  Based on this, we verify the user's permission by reading the payload of the JWT and extracting token.payload.permission from it.  The following notes clearly describe these steps.

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

# Assigning user rolesuser_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 = falseallow {
    # 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}
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • twenty one.
  • twenty two.
  • twenty three.
  • twenty four.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.

Step 11: Update the existing plugin configuration with the OPA plugin

Once the policies are defined on the OPA service, we need to update the existing plugin configuration to route to use the OPA plugin.  As shown in the following code snippet, we need to specify it in the policy attribute of the OPA plug-in.

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
      }
   }
}'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

Step 12: Test Setup Using OPA

At this point, we can test all settings using the OPA policy.  Once you run the following curl command to access the API Gateway endpoint, it will first check the JWT token during the authentication process, and then send the user and JWT token data to the OPA during the authorization process to verify roles and permissions.  Obviously, any request without a JWT token, or with an allowed role, will be rejected.

curl -i http://127.0.0.1:9080/speaker/1/topics -H
'Authorization: {API_CONSUMER_TOKEN}'
  • 1.
  • 2.

summary

In this article, by customizing a simple policy logic, we show how to allow or prohibit access to API resources according to the roles and permissions of API users.  At the same time, we also demonstrated how to extract the information related to the API user in the policy file from the JWT token payload sent by APISIX or the user object, so as to finally realize the RBAC effect of OPA and Apache APISIX.