Authorization in microservices: Patterns, pitfalls, and how to scale it

AAlex OlivierJuly 15, 202610 min read
Authorization in microservices: Patterns, pitfalls, and how to scale it

In a monolith, authorization is almost an afterthought. There is one codebase, one database, and one place to ask whether a user can do a thing. Split that monolith into thirty services and the question does not get thirty times harder, it changes shape entirely. The user context lives in one service, the data lives in another, the rule that ties them together lives in a third, and each team writes its own version of the check. What was a single guard becomes scattered logic that no one owns and no one can see end to end.

That is the real problem with authorization in microservices, and it is an architecture decision, not an implementation detail. Get it right early and access rules stay consistent and cheap to change as you add services. Get it wrong and every new service, every refactor, and every acquisition adds to a pile of bespoke permission code that becomes visible only during a fire drill.

This guide walks through why authorization is harder in a distributed system, where it gets enforced, the patterns teams use to handle it, and the approach that actually scales.

Monolith decomposing into services.png

Why authorization is harder in microservices

Three things change when you move from a monolith to services, and each one makes authorization harder.

The data spreads out. In a monolith, deciding whether a manager can approve an expense is a query against tables that sit in the same database. In microservices, the manager's role might live in an identity service, the expense in a finance service, and the team structure in an HR service, so the single decision now depends on data owned by three teams and reachable only over the network. Chris Richardson's microservices patterns work covers this scattering in depth.

The enforcement points multiply. A monolith has one front door. A microservices system has many, an API gateway, service-to-service calls, event consumers, and each is a place where a request has to be authorized. Miss one and you have a gap, which is how broken access control, the top risk on the OWASP list, tends to happen in distributed systems.

The rules drift. When each team implements its own checks, the same concept gets built five slightly different ways. One service treats an admin as unrestricted, another scopes admin to a tenant, and reconciling them becomes its own project. Consistency, not cleverness, is the hard part at scale.

None of this is a reason to avoid microservices. It is a reason to treat authorization, and the wider security and access control requirements of a microservices environment, as a shared concern from the start rather than something each service reinvents.

Where authorization gets enforced

Before choosing an approach, it helps to see the places a request can be checked, because a real system uses more than one.

At the edge or ingress, you handle coarse checks, is this request authenticated at all. At the API gateway or backend-for-frontend, the API layer your UI talks to, you can attach identity and do request-level authorization before anything reaches your services. Inside the service mesh, sidecars can enforce which services are allowed to talk to which. And within each service, you make the fine-grained decision that depends on the specific resource, can this user edit this record. NIST's guidance on attribute-based access control for microservices, SP 800-204B, describes this per-request pattern in a service mesh.

The important idea is to separate two things that get conflated. Enforcement is naturally distributed, it happens at every one of those points. The decision does not have to be. You can, and should, keep the logic that makes the decision in one place, and let the many enforcement points ask it. That separation is what the rest of this guide builds toward.

The three patterns for handling authorization data

Because the data a decision needs is spread across services, teams end up choosing one of three patterns.

Pattern Details
Leave the data where it is and have each service fetch what it needs at decision time. This keeps ownership clean but adds a network call, and sometimes several, to every authorization check, which hurts latency and couples services together.
Have a gateway attach the relevant data to each request, usually inside a token, so it travels with the call. This is fast, but tokens have size limits, the data can go stale between issue and use, and verifying large signed tokens on every hop has its own cost.
Centralize the decision. You move the rules, and the context they need, into one authorization service that every part of the system calls, and it returns allow or deny. This is the pattern that scales, because it gives you one place to reason about access, one place to change it, and consistent answers everywhere. Centralizing the decision does not make the scattered data disappear, it moves the problem to one place where an enrichment layer can solve it. Cerbos Synapse, the commercial data layer of the Cerbos authorization platform, assembles the identity, resource, and relationship data a decision needs from your IdPs, databases, and APIs at request time, so a service sends a minimal request and gets a fully-informed decision back instead of gathering context itself. It is also the model behind externalized authorization, which we come back to below.

Choosing an access control model

Whichever pattern you pick, you still need a model for expressing the rules. Role-based access control is the usual starting point, a user has a role, the role grants permissions. It is simple and it is what your identity provider hands you, but on its own it cannot express rules that depend on the specific resource or the context of the request, and in a distributed system those rules are the norm.

That is why microservices tend to push teams toward attribute-based access control, where the decision draws on attributes of the user, the resource, and the request, and toward policy-based access control, which centralizes those attribute rules into explicit policies.

Relationship-based models exist too and suit domains shaped like a graph, where access follows the relationships between records.

The practical answer for most teams is to keep roles for the broad strokes and layer attribute conditions on top for everything finer, which is the essence of fine-grained authorization.

User authorization is not the whole story

One thing that catches teams out is that in microservices, a lot of the traffic is not users at all. Services call other services, jobs run on schedules, and agents act on their own. Those non-user principals need authorization too, and a role designed for a human does not fit a workload. Deciding whether the billing service may call the ledger service, or an AI agent may invoke a tool, is service-to-service authorization, and it uses the same policy engine but with workload identities as the principal.

Treating it as a first-class concern rather than an afterthought is part of a proper zero-trust posture, where every interaction is verified rather than trusted because it came from inside the network.

The pattern that scales, externalized authorization

Put the pieces together and a shape emerges. You keep enforcement distributed, at the gateway, the mesh, and inside each service, and you centralize the decision in a dedicated policy decision point. Your services stop carrying authorization logic and instead make one call to ask whether an action is allowed.

In practice the rules live as policies, versioned in Git, and a stateless decision service evaluates them. Here is what a rule looks like, a plain policy rather than code scattered through handlers.


apiVersion: api.cerbos.dev/v1
resourcePolicy:
 version: default
 resource: expense
 rules:
   - actions: ["approve"]
     effect: EFFECT_ALLOW
     roles: ["manager"]
     condition:
       match:
         expr: >
           request.resource.attr.department == request.principal.attr.department &&
           request.resource.attr.amount <= request.principal.attr.approvalLimit

Every service that needs to authorize an approval calls the same decision point with the principal and the resource, and gets a consistent answer. Because the decision point is stateless, you run it as a sidecar next to each service, so the check is a local call rather than a network round trip, and it scales horizontally like any other stateless component. The open-source Cerbos PDP is the engine built for exactly this, with policies in human-readable YAML and conditions in a straightforward expression language.

This is what makes the model hold up as you grow. Running one decision point is easy, keeping dozens of them consistent across services is the hard part, and that is where the commercial control plane earns its place. Cerbos Hub distributes policy updates to every PDP instance without a restart, so a rule change reviewed in a pull request lands across all your services at once rather than as a coordinated redeploy, and product or security can adjust access without pulling every team back in. It also pulls the audit trail from every instance into one place, which turns per-service fragments into a single compliance-ready record for SOC 2, ISO 27001, and HIPAA. The rules still live in one source of truth. And for the list endpoints that microservices are full of, a query plan turns a policy into a database filter, so a service returns only the records a user may see without checking each row.

Teams run this at real scale, Utility Warehouse makes authorization decisions across their service mesh millions of times a day this way.

cerbos - how it works (1).png

The build or buy decision every engineering leader faces

You can build this yourself. A shared authorization library, a token convention, and some middleware will get a few services talking. The question is what it costs over three years, not three weeks. Every new service adopts the library, every rule change ripples across teams, and the library becomes a piece of internal infrastructure with its own maintainers, its own bugs, and its own audit-logging and testing that someone has to build.

Authorization is a solved problem, and rebuilding it per company means spending senior engineering capacity on something that does not differentiate your product.

The honest version of the build versus buy call is that building is fine while you have one or two services and stable rules, and it stops being fine right around the point microservices make it interesting. If you are evaluating options, we put together a framework for comparing authorization solutions that lays out the criteria that matter, latency, policy expressiveness, language support, and operational model, so the choice is made on evidence.

If you are migrating from a monolith

Most teams do not design microservices authorization on a whiteboard, they arrive at it while pulling a monolith apart. That transition is its own project, because the single authorization point you relied on disappears the moment you split the first service out.

We covered the specific reasons a migration demands a distributed authorization model, and the earlier work of drawing service boundaries that determines where those authorization seams fall. Externalizing the decision before you split is the move that saves the most pain, because each new service inherits the same authorization layer instead of growing its own.

A short checklist for getting it right

A few practices separate a microservices authorization setup that holds up from one that becomes a liability:

  • Keep the rules in one place so the full picture of who can do what is readable rather than pieced together by searching through the code.
  • Make decisions contextual, not just role-based, because distributed systems are full of "only their own" and "only under this amount" rules.
  • Log every decision consistently across services so an audit is a query, not an archaeology project.
  • Plan for growth by choosing an approach that a new service adopts in an afternoon.
  • Apply least privilege to services and workloads, not only to people.
  • And keep the transport secure, since authorization assumes the request it sees is the request that was sent.

Where to start

The fastest way to see the pattern is to externalize the decision for one service and grow from there. If you want hands-on guides, we have walkthroughs for Express, Go, and Python, and the quickstart gets the open-source policy decision point running in a few minutes. When you need to run it across many services, Cerbos Hub, the commercial control plane, handles policy distribution, versioning, and audit-log aggregation for you.

Try Cerbos Hub to manage and distribute authorization policies across every service, or book a call to talk through your architecture with the team.

Go deeper:

FAQ

What is authorization in microservices?

Why is authorization harder in microservices than in a monolith?

What are the patterns for handling authorization data in microservices?

Should you use RBAC or ABAC for microservices authorization?

How do you handle service-to-service authorization in microservices?

Should you build or buy authorization for microservices?

Free policy workshop

Get your first Cerbos policy written by our team.

Book a session to talk through your requirements and walk away with a working policy.

Book a session