Decentralized Authorization with the Open Policy Agent

Dominik Henneke
SDA SE Open Industry Solutions
6 min readFeb 10, 2020

Learn how we deal with complex authorization rules in a distributed microservice architecture.

Photo by Eric Prouzet on Unsplash

There is a good reason that private data like your address, your credit score, or your health records are not accessible to everyone. But what if some of these data should be accessible to others? Like the medical records of your child that is covered by your health insurance contract and that you want to access? And what if you are building a microservice-based application that needs to implement this use case? And the next setup with new specialized authorization requirement is right around the corner?

Our Job

We face these problems on our daily basis. As an InsurTech we help the insurance industry to transform themselves into the digital age. Following a product driven approach, we build microservices as self-contained systems. These products can be used as-is, should fit into the solutions that are developed by our customers, or connect with an ecosystem of partners. Our services run in different deployment scenarios with different requirements in terms of authentication and authorization.

Access Control 101

You can divide the process of securing access to data or services into two individual steps:

  1. Who are you? (Authentication)
  2. Are you allowed to be here? (Authorization)

The OAuth 2.0 framework and the OpenID Connect (OIDC) protocol provide a solution for delegated authentication and authorization of human users and technical services. We use them in the first step to receive a user identity in a signed JSON Web Token (JWT) that we transport in the Authorization header of HTTP-based communication.

When it comes to authorization, the standard solutions that fit our needs were not so obvious. We don’t want to use an edge security model where we check a user identity only at an ingress API-Gateway and trust all internal communication. We also don’t feel that solutions such as XACML, the scope-based approach of plain OAuth 2.0, or the resource centric UMA are the right choices for us. They are too limiting for our demands: We want to build a distributed authorization as close to a data source as possible. And we don’t want to build the authorization logic directly into our services. This gives us and our customers the flexibility to adapt the rules to specific environments without changing the service implementation.

Entering the Open Policy Agent

So what do you do if you are not satisfied with a known solution? Y̶o̶u̶ ̶b̶u̶i̶l̶d̶ ̶y̶o̶u̶r̶ ̶o̶w̶n̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶. You read tech-blogs, watch tech talks, and take a look on the CNCF Landscape. There we stumbled across the CNCF incubating project Open Policy Agent (OPA). A perfect match for policy evaluation for fine-grained access control.

[The OPA] provides policy-based control for cloud native environments and flexible, fine-grained control for administrators across the stack.

At the moment, the most interest in the OPA is its use as a policy-controller for Kubernetes, though there are many more use cases like config file linting or API authorization. There is a growing list of adopters that use the tool for different use cases.

Authorization Architecture

So what do we need? A modern, flexible, independent, configurable, and distributed authorization infrastructure that is cloud-native and can be integrated into all kinds of services, written in a wide variety of programming languages. We want to separate policy authoring and policy decision from the actual service implementations, but want the policies to still be part of the self-contained systems that our services represent. We want to create explicit authorization interfaces that are part of the service description like all other APIs, may it be synchronous HTTP or gRPC or asynchronous Kafka.

Our abstract authorization concept using sidecars to compute policy decisions. You might remember the terms PDP, PEP, and PIP from the well-known XACML standard.

We decided that the Open Policy Agent should take over the role of the depicted Policy Decision Point (PDP) that instructs the Policy Enforcement Point (PEP) on whether to accept or deny the request. In addition, it can provide more detailed information that the service uses to restrict access to only a subset of data that the user should actually see. This comes in handy when we are authorizing the results of search or list endpoints and the OPA is not aware of which data is actually stored in the service’s database.

Policy Example

This is an example of a small authorization policy for a fictitious service. It is defined in a rego file and is accompanied by an example input that was used to call the policy. You can also access this example in the Rego Playground.

Policy that allows access for all requests that present a valid JWT and limits the access to only the own user.

Designing Authorization Interfaces

We opted for an approach to design a dedicated authorization for each microservice. All policies must contain an allow rule that decides whether a user is allowed to access a resource in the first place. If the service itself needs further information about the user, the authorization API can be defined accordingly. Sometimes the policy is simply used to extract the user-id of a caller from the JWT, like in the policy example above. The user-id might be stored in different claims in different deployments or might need specific transformations that are configurable by the policy without the need to maintain different service versions for different environments and customers. Sometimes the policy evaluates an ACL and returns a list of user-ids that the caller can access data from.

In our current setup, we limit our OPA usage to securing RESTful HTTP APIs. When calling the policy, we provide the HTTP verb, path, a validated JWT, and all request headers as input to the OPA. The OPA then returns allow and a defined list of service-specific constraints.

There are ideas of generating SQL statements from the OPA response, however, we opted for an approach with more concrete functional authorization rules and an explicit use in our services. We also didn’t use the integration of the OPA in the envoy sidecar of e.g. Istio environments yet, but call the OPA sidecar directly from the service.

Implementation Example

How might an implementation of our concept look like? Here is a simplified example from one of our services:

Service: There is a service that stores user information (name, address, phone number, etc) that are consumed by various other services.

Functional Authorization Requirements: 1. A user should only access its own data. 2. There might be administrative users or other services that should access all users.

Authorization Constraint Modeling: We need a constraint “allowedUserIds” that returns a list of allowed user ids and a boolean field “fullUserAccess”.

Service Documentation: We document the policy API in our service:

You can find a set of default OPA policies inside the policies/ folder. Policies are expected to have the following outputs:

- allow _boolean_: If true, access to the resource is granted.
- allowedUserIds _[string]_: A list of user ids that should included in the response. If _null_ or empty, no user is returned.
- fullUserAccess _boolean_: If true, all users are returned independent of the values from allowedUserIds.

Service Implementation and Tests: We consume the OPA response by mapping it into a Java class.

A constraint model that receives the response from the Open Policy Agent in a Java service.

Afterwards, these rules are implemented in the service as described in its documentation so that the policy authors can decide how to fill these information to achieve the planned result. We should mention here that it is of course crucial to write tests in both the service and the policy to ensure that the agreed authorization logic is always fulfilled on both ends.

If you want to have a look on how we implement the interaction with the Open Policy Agent in our services, you can find our library that we use for our Java-based services here:

We also built a module to support the developers when writing the tests on the Java side: https://github.com/SDA-SE/sda-dropwizard-commons/tree/master/sda-commons-server-auth-testing#opa-rule

What’s Next?

There would be much more to tell about our authorization approach. How to deploy and operate the OPA in our Kubernetes deployments? How to maintain and distribute the policies? How to help our customers to adjust the policies to their needs? How to create and load additional datasets that is used in policies? How to audit the policy decisions? How to implement policy governance? …

I can tease that both OPA and our processes provide and defined lightweight and straightforward solutions for all these questions. But that might be part of another blog post.

--

--