Using OPA To Achieve Zero-Trust APIs

Using OPA To Achieve Zero-Trust APIs

Posted in

OPA, or Open Policy Agent, is a strong standard for handling authentication and identity within a distributed zero-trust model. Exactly how it does that — and why it should be doing that — was the subject of a talk given recently by Anders Eknert from Styra at the Nordic APIs 2023 Platform Summit in Stockholm, Sweden. Below, we’ll discuss this topic and dive into the proposed solution delivered by Eknert.

Watch Eknert present about using OPA for zero trust at the Platform Summit:

The Evolution of API Authorization

As APIs have changed, so too have their mechanisms for authorization. In the early days of API development, codebases tended to be monolithic. Massive bodies of code designed for an application existed as a single entity.

“The monolith basically means the application does everything — [it] does authentication, it does access control, it has to verify the identity of the user, it has to verify that the authentication and access control are done in one single place.”

While this was acceptable for the era, it came with various negative outcomes. Monolithic codebases were hard to change and upgrade, as even a minor update required the entire stack to be rebuilt. The code was typically inefficient, as scalability and extensibility were wholly impactful, regardless of how small the alteration actually was. While they granted strong controls, they were quite expensive and required a support apparatus that could become large and unwieldy.

Over time, APIs began to change. The monolith gradually fell out of favor for a new solution — distributed microservices. Microservices were comparatively more complex because the monolith exploded into multiple pieces, but this complexity granted huge benefits to process efficiency, extensibility, and scalability.

However, one major question in this new paradigm was the authorization and authentication process. In a monolithic service, this was handled by a single agent, a single process, and all was well. In a distributed service, however, the question was not resolved. After all, when multiple systems are involved, things get a lot more complicated.

Identity in Distributed Systems

Something had to change. The monolithic approach to handling authorization and authentication could no longer stand in the distributed and microservice-heavy modality of the future.

“[There are] a whole bunch of problems with this model. [On the identity side], we’d rather not pass around credentials between services. That is problematic from a whole bunch of standpoints… The more services you get, the more this is going to be a bottleneck.”

Accordingly, some shifts began to take place. Where identity management was once embedded within the application itself, these new design paradigms required external identity systems to control the flow. This improved scalability and security but also introduced further caveats.

One concern was in the domain of trust. When you adopt scalable external services as a solution, you need to establish a system where trust can be maintained or, at the very least, verified. Another huge issue is portability. Distributed systems rely on the portability of systems, so any solution for authentication (and authorization) would need to be portable.

Token-Based Authentication

A solution arose in the adoption of token-based authentication. The idea was simple — by using something like JSON Web Tokens (JWTs), a piece of data (a token) could be created holding the authentication information from the service in question. This token could then be brought by request to other systems, allowing for high portability throughout the distributed system without repetitive queries or checks.

This required some new tooling to be created. The older systems simply did not have the functions needed to do this well. While the idea of tokens was not new per se, implementing them within the distributed system was novel. It required some new solutions to manage the creation, management, and revocation of these systems.

OAuth 2.0 and OpenID Connect

A major issue with early token-based authentication was that these systems were often limited only to the service in which they were created. Without having a standard mechanism, you could only validate the interaction on the service that created the token. This was fine for some use cases, but in the era of mass-distributed systems, this was simply not good enough. There was a sudden need for mass-distributed token standards.

One such combination of tools created to meet this need was OAuth (currently OAuth 2.0) and OpenID Connect. The core concept was simple: by creating an industry-standard protocol for authorization and then layering it with an identity service to facilitate this authentication, tokens could be created for internal systems that could also be ported to external services.

While this allowed for standard tokens and user attributes, there was a caveat — there was very little in the way of fine-grained authorization. While you could hack your way around this, the technology did not define a robust solution for fine-grained controls.

“The OAuth authorization framework doesn’t really solve the problem of authorization — at least not on a more fine-grained level, which is what we’re looking for.”

Challenges in Distributed Authorization

In the same vein, while there was no definition for fine-grained control through tokens, there was also no definition for exactly how these credentials should be passed. There were some defined flows and optional methodologies, but the way services could pass these credentials was ill-defined.

To add to the mess, there was a concern around complex authorization needs. With complex setups, these issues were only exacerbated, with different levels of consideration, domains of control, and more adding unknowns and difficulties into the flow. There was also a major concern of trust — ironically enough, without a clear methodology to determine and control credential flow, there was a weakness in defining specific mechanisms for trusting other verification services.

Zero-Trust: A Forward-Thinking Model

Enter the zero-trust model. This model is straightforward — every service does its own authentication and authorization for the functions in question and assumes that all services — even internal ones — are untrusted. This additional layer of verification essentially adopts a practice of questioning everything while accepting the veracity of outside services.

Think of it this way. In a zero-trust architecture, a service might look at a token-bearing request and say, “Yes, I trust that you’ve come from this other service, but you have to pass my tests as well”. This way, the specific request can be accepted as coming from a specific service, but the functional request can still be validated and verified.

This approach solved a lot of issues. And increased security demands had caused concerns about ensuring all services aligned to the same standards. Zero-trust models made it so that it didn’t matter how security differed between services, as the function would still have to be validated when it arrived.

This substantially decentralized the authorization logic, allowing services to set permissions directly instead of relying on an external centralized database. In many ways, this was the obvious next step in the adoption of decentralizing APIs. This also led to significantly reduced latency and external dependencies, making development more effective and portable over the long term.

“We still have a few more problems with this model, though. One of them is the authorization logic embedded in the services. The more [and] the larger your organization, the more complex it is, the more heterogeneous, the more of these programming languages and frameworks you introduce — these will all do authorization in their own specific way. And, of course, they will all have their specific bugs related to authorization. This is a nightmare to audit.”

This model, on its own, has some issues that are persistent. Decentralizing authorization has many benefits, but it does introduce substantial complexity into the system, which can ultimately make the situation worse, not better.

Open Policy Agent (OPA)

Open Policy Agent takes this one step further, presenting a unified but decentralized open-source solution for setting standards for authorization. By adopting OPA, you get the best of both worlds — a decentralized stack that still has a unified policy engine. This engine can help delegate authorization decisions while enforcing more granular control across the service, allowing systems to communicate effectively while adopting a standard implementation.

Rego Language for Definition

To set these policies, a language was needed. Rego, a declarative language, was adopted as the language of choice. This language allows for very clear and specified requirements to be set and enforced across various platforms, reducing the impacts of differential frameworks, languages, and systems.

Implementation and Enforcement

In practice, the system works by decentralizing the decision logic as a flow of its own. In this process, there are several elements:

  • Service: The system in question which has had a query made to it.
  • OPA: The agent that routes the query through to the policy engine.
  • Policy: The policy definition that allows for strict decision-making and authorization choices.
  • Data: Additional context around external deployment context.

In the standard flow, a service passes a query through OPA. At this point, OPA splits the request in several directions. First, it routes the request authorization itself to the Policy engine. From here, the engine can review the information at hand and decide whether the request can actually be fulfilled. At the same time, additional data around the deployment context can be provided at this time, allowing for more context-driven decisions.

Once a decision is rendered, it’s fed back through OPA and then to the service for proper response. This decentralized ability to set standards allows multiple systems to connect to a highly granular and deployment-independent policy. This is a huge leap towards enabling zero-trust, enabling each service to own its centralization logic regardless of the method or modality of interaction.

Managing At Scale

The reality of adopting this strategy means that a substantial management service is needed to actually control these standards in some cases. With increasing complexity, you begin to need some sort of collation or collection point. For APIs, this is often the API gateway — a single unified point by which all interactions can be filtered and seen. For policy management, however, this gets more complex.

“When you start to have this for hundreds or thousands of services, you’ll need some way of managing all this to control that.”

Ecknert suggests using Styra, a commercial control plane, for this purpose. Yet, the main point is to have an appropriate solution for your use case. At this level of policy management, complexity will become your biggest issue, and how you manage this complexity will ultimately pay huge dividends.

Conclusion

OPA is a great solution for enabling zero-trust APIs. Adopting the decentralization paradigm is hugely beneficial and allows for more granular controls across multiple services without adopting a centralized solution.

What do you think of OPA as an authorization solution? Let us know in the comments below! Additionally, let us know of any other software architecture design trends you’d like to see covered on Nordic APIs!