How To Add RBAC to an Application Without a Single Line of Code

Amit Kanfer April 26, 2021
Add RBAC without Code

How To Add RBAC to an Application Without a Single Line of Code

Offering scalability and flexibility, microservices architecture has taken the software world by storm, and it’s here to stay for the long term. Just about every application, service, and function out there these days is separated and deployed as containers. On top of that, auxiliary services like logging, monitoring, and authorization are deployed as separate services into clusters.

Developing an app without having to think about who can access it has pretty much become an everyday use case. But when you’re ready to deploy and make your service publicly available, you need to take measures to restrict access. Specifically, you need to implement API authorization to restrict access based on user roles, known as role-based access control (RBAC).

In this blog, we’ll learn how to add authorization based on RBAC to an application with zero code. But first, a little background on Envoy and Open Policy Agent (OPA), the two main components in our solution.

Envoy

Envoy is an L7 proxy service designed for large cloud-native applications. Its mantra is that the network should be transparent to the app in order to identify root problems quickly. But easier said than done; this is so complicated to achieve, and Envoy provides a wide variety of features that make this possible.

Envoy’s five most important features include:

  • Out of process architecture
  • HTTP L7 routing and filter architecture
  • L3/L4 network filter architecture
  • Service discovery and dynamic configuration
  • Advanced load balancing

Additional features are covered in-depth in Envoy’s official documentation.

Open Policy Agent (OPA)

Open Policy Agent (OPA) is a policy engine for managing and unifying policy enforcement in cloud-native applications, microservices, Kubernetes, and API gateways. It provides a declarative language to define authorization policies and separates the policy decision-making from actual applications.

From 1,000 feet, the overall flow starts with an incoming request to service. The service then queries OPA, which uses the available data and policy. Next, OPA sends back the decision, and the service acts upon it:

OPA-graphic

Figure 1: Overview of OPA and services (Source: OPA Documentation)

The next step is creating an app and securing it with Envoy as a reverse proxy and OPA as a policy manager without changing the application’s code.

No-Code Authorization in Action

It’s a common scenario to secure an already running microservice with authorization. The microservice could have been developed by other teams or companies, or in an even more dramatic scenario, it may have been left to you by the previous developer.

In such cases, it’s important to make sure you don’t alter the app itself while wrapping it with authorization logic.

Say we have an app packaged into the container openpolicyagent/demo-test-server:v1 without any authorization. It’s a simple REST API server for managing users with the /people endpoint and GET, POST and DELETE actions. Run it locally, and list the users with the following commands:

$ docker run -d -p 8080:8080 openpolicyagent/demo-test-server:v1 
$ curl -s localhost:8080/people | jq
[
  {
    "id": "1",
    "firstname": "John",
    "lastname": "Doe"
  },
  {
    "id": "2",
    "firstname": "Jane",
    "lastname": "Doe"
  }
]

 

Now, create a new user and list users again:

$ curl -s -d '{"firstname":"Free", "lastname":"World"}' -X POST localhost:8080/people | jq
{
  "id": "498081",
  "firstname": "Free",
  "lastname": "World"
}
$ curl -s localhost:8080/people | jq
[
  {
    "id": "1",
    "firstname": "John",
    "lastname": "Doe"
  },
  {
    "id": "2",
    "firstname": "Jane",
    "lastname": "Doe"
  },
  {
    "id": "498081",
    "firstname": "Free",
    "lastname": "World"
  }
]

 

If you’re not an extreme libertarian, a production setup where everyone can play around without any access rules and policies will feel like a nightmare!

Shield Up the Application

Let’s continue by creating a single-node Kubernetes cluster using the command minikube start and wrap our demo application with authorization.

We’ll use an extended version of OPA to enforce policies with Envoy Proxy. The extended version’s name is OPA-Envoy, and it provides a gRPC server for the Envoy External Authorization API. The indisputable advantage of this setup is that you can use fine-grained OPA policies with Envoy Proxy without modifying your microservice application.

Next, we’ll deploy three ConfigMap resources and one deployment with three containers. The configuration resources can be summarized as follows:

  • proxy-config: Envoy configuration to work with the external authorization filter to redirect incoming requests to the OPA-Envoy sidecar.
  • opa-envoy-config: Configuration for the sidecar to know gRPC authorization extension.
  • opa-policy: Consists of a policy file defined in the format of rego to define authorization rules.

The deployment consists of one init container and three containers:

  • proxy-init: Init container to configure the iptables and redirect traffic to the Envoy Proxy instead of the application.
  • envoy: Proxy service, which is extended by the configuration provided in proxy-config.
  • opa-envoy: Extended version of OPA configured by opa-policy and opa-envoy-config.
  • app: The demo server that will be authorized.

Deploy all resources with the following command:

$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/opa-envoy-plugin/master/quick_start.yaml

deployment.apps/example-app created
configmap/opa-envoy-config created
configmap/proxy-config created
configmap/opa-policy created

Next, wait until the pod is in the Running stage:

$ kubectl get pods

NAME READY STATUS RESTARTS AGE
example-app-7c45cbf6df-jh8qt 3/3 Running 0 2m7s

Expose the deployment using NodePort and access from your local workstation:

$ kubectl expose deployment example-app --type=NodePort --name=example-app-service --port=8080

service/example-app-service exposed
🏃 Starting tunnel for service example-app-service.
|-----------|---------------------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|---------------------|-------------|------------------------|
| default | example-app-service | | http://127.0.0.1:64837 |
|-----------|---------------------|-------------|------------------------|
http://127.0.0.1:64837

❗ Because you are using a Docker driver on darwin, the terminal needs to be open to run it.

Get the address printed out in the previous step and export as SERVICE_ADDRESS in another terminal:

$ export SERVICE_ADDRESS=http://127.0.0.1:64837

$ curl -i $SERVICE_ADDRESS/people
HTTP/1.1 403 Forbidden
date: Thu, 18 Mar 2021 22:21:08 GMT
server: envoy
content-length: 0

It looks like there’s a barrier already set for our sample app, as we can’t access /people endpoint freely.

Roles and Access in Action

Now, let’s check why we received a 403 Forbidden response. In the following configuration, we’ve defined allow when both is_token_valid and action_allowed:

$ kubectl get configmap opa-policy -o jsonpath='{.data}' | jq -r '."policy.rego"'

package envoy.authz

import input.attributes.request.http as http_request

default allow = false

token = {"valid": valid, "payload": payload} {
  [_, encoded] := split(http_request.headers.authorization, " ")
  [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
}

allow {
  is_token_valid
  action_allowed
}

is_token_valid {
  token.valid
  now := time.now_ns() / 1000000000
  token.payload.nbf <= now
  now < token.payload.exp
}

action_allowed {
  http_request.method == "GET"
  token.payload.role == "guest"
  glob.match("/people*", [], http_request.path)
}

action_allowed {
  http_request.method == "GET"
  token.payload.role == "admin"
  glob.match("/people*", [], http_request.path)
}

action_allowed {
  http_request.method == "POST"
  token.payload.role == "admin"
  glob.match("/people", [], http_request.path)
  lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub)
}

 

Let’s create two tokens for admin and guest roles and sign them with a very secret key: secret. You can play around with the fields and create the tokens using an online tool such as jwt.io:

token guest role

token admin role

Next, use the tokens in your terminal:

export GUEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJlY2UiLCJuYmYiOjE2MTYxMDY3ODIsImV4cCI6MTc0MjMzNzE4Mn0.1aXL_e4AnWEUOD3iF_zdUW14i9RSTFgRoyPZ7cIe30Q
export ADMIN_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJudXIiLCJuYmYiOjE2MTYxMDY3ODIsImV4cCI6MTc0MjMzNzE4Mn0.PxYp3VYAJCRsoGtb3vMOYUOYh45VRiJudKDClQV03Y8

Then, ensure that guest users are authorized for listing but not for creating new users:

$ curl -s -H "Authorization: Bearer "$GUEST_TOKEN"" http://localhost:64837/people | jq
[
  {
    "id": "1",
    "firstname": "John",
    "lastname": "Doe"
  },
  {
    "id": "2",
    "firstname": "Jane",
    "lastname": "Doe"
  }
]
$ curl -i -H "Authorization: Bearer "$GUEST_TOKEN"" \ 
-d '{"firstname":"Limited", "lastname":"World"}' \ 
-X POST $SERVICE_ADDRESS/people 

HTTP/1.1 403 Forbidden 
date: Thu, 18 Mar 2021 22:48:13 GMT 
server: envoy 
content-length: 0

 

Next, make sure admin users can also add a new user:

$ curl -i -H "Authorization: Bearer "$ADMIN_TOKEN"" \
-d '{"firstname":"Admin", "lastname":"Access"}' \
-H "Content-Type: application/json" \
-X POST \
$SERVICE_ADDRESS/people

HTTP/1.1 200 OK
content-type: application/json
date: Thu, 18 Mar 2021 22:52:26 GMT
content-length: 56
x-envoy-upstream-service-time: 0
server: envoy
{"id":"498081","firstname":"Admin","lastname":"Access"}

 

As you can see, it looks like we’ve already added a layer of authorization to our application without making any modifications to it.

Conclusion

To sum up, in this blog, we focused on adding a flexible but strong layer of authorization to a microservice, all while following cloud architecture best practices and separation of concerns. As you saw, the app itself had no authorization logic or code and was freely available. We added a proxy and policy decision-making sidecar container to wrap it with authorization. And without changing a single line of code, we enabled RBAC for the microservice.

Simplify authorization and build application RBAC and ABAC in minutes with fine-grained access controls and decoupled logic. Book a demo of the build.security OPA-powered platform.

 

Subscribe to build.security’s newsletter

Keep up with the latest news on our authorization policy management platform