Styra sponsored this post.
There are unquestionable advantages to cloud native technologies, but significant challenges as well. Case in point: microservices authorization.
Microservices have, for many companies, become the architecture of choice for cloud native apps — whether for migrating legacy apps or building new cloud native applications. Contrary to traditional, monolithic applications, a microservice architecture means that each app is composed of dozens or hundreds of individual services, each with its own set of APIs.
In a microservice architecture, developers are in the tricky position of securing not just a single external API gateway, but each individual microservice API with a secure authorization step. Indeed, a core tenet of a zero-rust architecture is that every request must be both authenticated and authorized.
For developers and security teams, the thought of implementing authorization for every microservice call can seem daunting. Fortunately, there are a number of best practices to get you on your way — and standardize on a scalable process across teams.
Decouple Authorization Logic and Policy from the Underlying Microservice
This is the big one. Simply, the most powerful step that teams can take in authorization is to decouple authorization logic and policy from the application itself — that is, refrain from hardcoding authorization logic into microservices. This allows teams to easily change authorization coding for policies without changing the coding for the app.
Even better: Standardize your authorization on a tool like Open Policy Agent (OPA), which allows you to create and enforce policies consistently across services and teams. Because OPA is so flexible, this also grants you a wide range of options for how you actually architect authorization enforcement and where you store policy libraries within the app.
Use Sidecar Enforcement for Security, Performance and Availability
In the past, most authorization decisions have happened at the gateway — and developers can still enforce authorization there for microservices, if they like. However, for security, performance and availability, it’s typically preferable to also enforce authorization steps for each microservice API. As mentioned, in a zero-rust architecture, every request must be both authenticated and authorized before it is allowed.
It’s entirely possible to send each of these authorization requests to a centralized service. However, this can add significantly to latency — for instance, a single user request might traverse numerous services, and if each of those requests requires an additional network hop to reach that centralized authorization engine, that can hamper the user experience.
If you’re using a tool like OPA, fortunately, you can also run a local authorization engine and policy library as a sidecar to each microservice. Here is an example of what this architecture looks like with an Istio service mesh, which uses an Envoy proxy sidecar. Using this model, you can ensure that each request passes muster with an authorization check while maximizing the performance and availability of the service.
Enforce JSON Web Token (JWT) Validation
In keeping with the need to authenticate every user and every service-to-service request in a zero-rust model, it’s also a best practice to establish secure trust between these entities. Enter JSON web tokens (JWTs). These access tokens provide a unified way of verifying the identity of a caller — since they’re “signed,” issued by the trusted identity server in your architecture (like OAuth2 or OpenID Connect), you know you can trust the user IDs associated with them (they must match the public key associated with the token).
As a bonus, JWTs are “self-contained” entities: They record all the data (and metadata) you need to establish user access elements like roles and permissions — there is no need to make external calls to a centralized service to perform a lookup.
As with the previous tip, you can also accomplish this step (verifying JWTs) using OPA. Simply, you can create and force policies at the microservice API level that require token verification — for instance, that all requests to service A or service B must contain a valid JWT. Moreover, you can also include context-based authorization requirements — such as that the user must be a developer, currently on call and in a certain geographic location — that are needed to make a request. In this simple way, you can begin to more effectively enforce zero trust in your microservices cluster.
Use RBAC and ABAC to Control End-User Actions
In a similar way, it’s also a best practice to use role-based access control (RBAC) to control what end users are authorized to do in the cluster, based on their job function. Much more than a best practice, RBAC is the foundation of authorization at the application level. Broader than merely taking a username and assessing what permissions it has, RBAC allows you to group permissions into roles — developer, marketer, people operations or any other role. Then you can assign permissions to that role, which is easily assigned or revoked. This is a much simpler, more logical and scalable way of assigning permissions than on a per-user basis.
Here, you can easily enforce RBAC using a tool like OPA. In the sample policy below, we can see how users Alice and Bob are allowed permissions to perform different actions on different resources in the cluster, based on their respective roles in engineering and webdev (Alice) and human resources (Bob).
Note, too, that this is only the beginning of what is possible with permissions with OPA. Teams can also easily implement attribute-based access control (ABAC) requirements (alluded to above), which allows for much more fine-grained authorization — assigning attributes to users and resources and then defining logic between them. For example, you could create a policy that says users can only access service A (which contains critical user data) if they have spent at least two years at the company, or if they control a certain level of budget.
Once you begin with ABAC, there is no limit to the kinds of consistent, scalable authorization you can enforce. And one final point: With OPA, you don’t need to make an upfront decision about RBAC vs. ABAC — you can use what makes sense for your use case and easily transition between the two models.
Getting Up and Running with Authorization
Of course, these are some of the fundamentals of microservices authorization. You soon want to build out other secure functionality, such as incorporating SPIFFE/SPIRE to authenticate different services in a microservices architecture. Fortunately, there are plenty of resources to guide you on your way. Not only is there plenty of documentation on these matters, but you can take our free Styra Academy course to learn how to architect microservices authorization and deploy sample applications in Istio and Kuma service meshes alongside OPA.
While microservices will always present challenges to developers, these kinds of resources have simplified once-challenging tasks into standard procedures. Good luck and happy building!