Envoy ext_authz: enforcing one authorization policy at the gateway and in the service

AAlex OlivierAugust 14, 20269 min read
Envoy ext_authz: enforcing one authorization policy at the gateway and in the service

Most stacks end up running two access control systems that were never designed together. The gateway holds one set of rules, expressed as route matchers, token scope requirements and proxy filter config. The service holds another in application code, checking whether this principal owns this record and whether the record is in a state they may see. Both were correct on the day they were written.

Then they drift. A route ships and the gateway gets an entry the service check never learns about. A service tightens a rule and the gateway keeps waving requests through to a 403 the platform team has to explain. After a year, answering who can do what means reading a proxy config, reading a codebase, and reconciling them by hand. This is the coupling problem that pushed teams toward externalized authorization in the first place, reintroduced at the network boundary.

The enforcement point is not the missing piece. Envoy has shipped external authorization as a first-class HTTP filter for years, and the industry now has a standardized version of the same idea. The OpenID Foundation published the Authorization API 1.0 as a final specification in January 2026, defining how any compliant enforcement point asks any compliant decision point a question. What it deliberately leaves open is everything behind that decision point. Policy language, internal architecture and state management are all out of scope, and it assumes the caller passes in the attributes an evaluation needs. The interface is settled. How the decision point gets those facts is not, and that is what every integration still has to answer.

The gateway is already able to delegate

The ext_authz filter sits in the HTTP filter chain ahead of the router. For every matching request Envoy pauses and sends a gRPC CheckRequest to a configured cluster, carrying the path, method, headers and peer metadata. On allow, Envoy can attach headers before forwarding upstream. On deny, it returns a status of the authorization service's choosing and the request never reaches the backend. The filter can also be disabled per route, so health checks and other unauthenticated paths skip the call entirely.

What the filter must not become is a place where rules live. NIST SP 800-162 draws the boundary the way every serious reference architecture does, splitting an access control mechanism into a decision point, an enforcement point that carries out what it is told, and a context handler that assembles the attributes the decision needs. The enforcement point intercepts, asks and enforces. It holds no logic of its own.

Most gateway integrations stop short of that, pointing the filter at something that only understands routes, so the service keeps running its own role based checks against a separate rule set and the drift starts again.

One address answers both questions

Cerbos Synapse implements the Envoy external authorization protocol natively. Envoy's ext_authz cluster points at it over gRPC, and the same listener serves the standard Cerbos API that application SDKs speak. That detail closes the gap. The gateway and the service call the same address, run their checks through the same engine, and read the same policy bundle.

Because the protocol support is built in, the integration is configuration rather than an adapter binary you build, ship and keep current with two upstreams at once. Synapse can embed a Cerbos PDP in the same process, so the decision does not fan out to a second hop, and it can point at an existing PDP fleet instead. Read against the NIST decomposition it is the context handler, the piece that assembles what the decision needs, which is precisely the role the standards leave undefined.

The mapping turns an HTTP request into a policy question

Envoy's CheckRequest is a network structure. A Cerbos check is a question about a principal, a resource and an action. Synapse bridges the two with CEL expressions in configuration, and the shape of that mapping determines what your policies can reason about.

A typical mapping pulls the bearer token from the authorization header, decodes the payload, and takes the subject and roles claims as the principal. The resource id is the request path, with path and method carried as attributes the policy can test. The action is a single fixed verb, because at the edge there is only one thing being asked. The raw token goes through as auxiliary data, so the decision point verifies the signature rather than the gateway or the service implementing that logic.

The response mapping runs the other way. An allow becomes an OK status, a deny becomes PERMISSION_DENIED with a chosen status and body. More usefully, policy outputs become request headers. When the gateway rule fires it emits the resolved principal id, roles and account identifier, and Envoy attaches those before forwarding. The decision does not only gate the request, it carries context forward, and a new output adds a header without touching proxy config.

This is the real design choice in any enforcement point. It can send only the instant attributes it alone knows, such as path, headers and caller address. It can send fully qualified requests with every derived attribute resolved up front. Or it can send minimal identifiers and let the decision layer resolve the rest, which is where a Synapse data source pulls current attributes from an identity provider or a database. Most deployments mix all three. The companion piece on identity attributes at decision time argues for resolving them late rather than trusting a token issued hours ago.

The service asks a narrower question against the same policy set

Past the gateway, the API has the request context it needs in headers and does its own check. It scopes its query to the requested account, builds a batch of document resources carrying status and owning account, and asks the same endpoint which of them this principal may read. Regular users get published documents from their own account, administrators get everything.

Both checks are resource policies in the same bundle. One describes routes, the other documents. They share a principal vocabulary and are reviewed and versioned together. The gateway policy enforces the account boundary so a cross tenant request is refused before it touches application code, and the document policy handles record state, which the edge cannot know. The batching here is the same pattern used to resolve a whole screen of permissions in one call.

What this changes about the architecture

Route level rules stop living in proxy configuration. They become policy, written in the same language and diffable against the application rules they sit next to. An auditor asking which roles can reach the admin route reads a policy file rather than a filter chain. With Cerbos Hub handling storage and versioning, a change lands at both layers at once, so there is no window where the gateway enforces last week's rules while the service enforces this week's. Both layers emit decision logs into one stream, and the mapping carries Envoy's request id into the check, so edge and record level decisions join on one identifier rather than by timestamp.

This shape is not a local preference. Germany's federal API authorization infrastructure, approved by its IT Planning Council on 17 June 2026 and published open source with twenty three architecture decision records, is built on the same split. The analysis of the blueprint states it plainly. "Coarse-grained permissions are governed centrally; fine-grained decisions stay with the domains that own the data; and the whole thing is designed so that a decision point keeps working even when the central plane is down."

Its founding thesis is profile, don't invent. The standards already exist, so they get composed rather than reinvented per service, and what gets composed includes OAuth 2.1, FAPI 2.0, DPoP and OpenID AuthZEN between the enforcement and decision points, chosen once for the whole federation.

Limits and costs worth stating plainly

The edge check only knows what is in the request. Path, method, headers, token claims. It cannot know whether a document is archived, whether an account is suspended, or whether a record has an owner. Anything depending on resource state belongs in the service, and pulling that state into the edge to make the edge smarter recreates the coupling this pattern removes.

The structural limit is larger than granularity. A gateway only enforces what routes through it. Being almost entirely configuration driven is its strength and its weakness, because any path reaching the service without passing the filter goes unauthorized. Internal callers, batch jobs and anything resolving the service by its cluster address bypass the edge completely. The service side check is not belt and braces. It is the part that survives a topology change.

The same argument arrives from the agent direction. CoSAI's agentic identity guidance, approved in March 2026, requires enforcement at agents, tools, APIs and data systems rather than only at perimeter gateways, because front door enforcement alone produces confused deputy failures as tokens propagate down a chain of calls. A gateway answers whether a caller may reach a route, not what an agent several hops in should do with what it found there.

Enrichment carries its own cost. Once a decision point resolves attributes from an identity provider or a database, that source joins the request path, and centralizing every attribute can move the bottleneck from authorization to the data layer. Caching answers the latency question and opens a correctness one, because a cached attribute is a claim about the past. Cache lifetime is a decision about how stale an authorization input may be, not a tuning parameter.

Then the plain latency cost. Every matched request waits on a gRPC round trip before routing. An embedded decision point removes the second hop and Envoy holds a multiplexed HTTP/2 connection, so there is no per request setup. Fail closed costs availability during an outage, fail open costs security, and that choice should be made on purpose rather than inherited.

This pattern extends to a mesh without being identical to one. Running the same checks on sidecars for east to west traffic brings workload identity in, which the Istio piece covers. The full mapping reference is in the Envoy extension documentation.

The boundary is drawn by data, not by ownership

The useful framing is not that authorization moved to the gateway. It is that the gateway and the service ask different questions of the same policy set, and the line between them is drawn by what data is available at each point rather than by which team owns the rules. The edge answers what it can from the request. The service answers what needs the record in hand. Neither keeps a private copy of the logic.

The standards settled how the question gets asked. Assembling the facts behind it stays an architecture decision, and it is the one worth the time. The mapping fields and deployment options are in the Synapse documentation, and the launch post covers the other protocols the same mechanism reaches.

Try Cerbos to see how this works in practice, or book a call to talk through your architecture with the team.

Go deeper: How to adopt externalized authorization (eBook) for a structured approach to moving authorization out of application code and into a policy layer

FAQ

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