Decentralized Authorization: Sidecars, Edge & Multi-Cloud
Where the PDP actually runs, made concrete: the sidecar pattern (Envoy ext_authz, OPA-Envoy, Istio/Linkerd authorization policies), running policy at the CDN edge before a request reaches your infrastructure, WASM as the portability layer that makes the same policy runnable in all these places, and what changes about policy distribution and the new enemy problem once the decision engine is spread across regions and clouds instead of living in one place.
The question the XACML article left open
Three articles ago, describing where a PDP can live, we wrote that it “can be a library compiled into the application, a sidecar running next to each service, or a central service every app calls over the network… a choice with real consequences for latency, freshness, and blast radius that the decentralized-authorization article develops in full.” This is that article. Everything covered so far in this module — XACML’s components, policy-as-code’s languages, Zanzibar’s relationship graphs, the granularity spectrum — has treated “where does the decision actually run” as a detail to fill in later. It’s time to fill it in.
The short version of why this is its own topic, not a footnote: placement is not just an infrastructure choice, it changes what the decision engine can promise. A PDP a network hop away can be more consistent but is a single point of failure and added latency on every request. A PDP running locally in a sidecar is fast and resilient to network partitions but now exists in hundreds of copies that all need the same policy. A PDP running at a CDN edge, potentially thousands of kilometers from your database, physically cannot see data that lives only in your origin region. None of these are bugs to engineer away — they are the actual shape of the trade-off, and a production authorization architecture has to choose deliberately among them, often using more than one at once.
The rest of this article walks that sketch layer by layer: the sidecar pattern first, then the edge, then the WASM portability layer that lets the same policy run in both, then how policy and data actually get distributed to every one of those copies.
The sidecar pattern
The most common way to decentralize a PDP today is the sidecar: a second process deployed alongside every instance of your application — in Kubernetes, a second container in the same pod — that your application (or, more often, a proxy in front of it) calls over localhost instead of over the network to another service.
The dominant concrete shape is Envoy + OPA. Envoy, the proxy most service meshes are built on, has an ext_authz filter that intercepts every request and asks an external authorizer for a decision before letting the request through to the application. Point that filter at a local OPA instance (the opa-envoy-plugin packages OPA specifically to speak Envoy’s ext_authz gRPC protocol) and you get authorization enforced transparently, in front of every service, without the application code ever calling an authorization library directly — the PEP lives in the proxy, not the app.
sequenceDiagram accTitle: A sidecar authorization flow using Envoy's ext_authz filter and a local OPA instance accDescr: The diagram shows a request arriving at Envoy, the proxy running as a sidecar container in the same pod as the application. Envoy calls the ext_authz filter, which sends the request to a second sidecar container in the same pod running OPA, over localhost, with no network hop leaving the pod. OPA evaluates the request against policy already loaded into its local memory and returns an allow or deny decision to Envoy in microseconds. If allowed, Envoy forwards the original request to the application container in the same pod. If denied, Envoy returns an error to the caller without the application ever seeing the request. Client->>Envoy: incoming request Envoy->>OPA: ext_authz check (localhost) OPA->>OPA: evaluate policy (in memory) OPA-->>Envoy: allow / deny Envoy->>App: forward (if allowed) Envoy-->>Client: reject (if denied)
This is exactly the “library vs sidecar vs central service” placement question made concrete: a sidecar keeps almost all of the library placement’s latency benefit (no real network hop, just a localhost call) while keeping policy out of the application’s own process and language — you don’t need an OPA client SDK in every language your services happen to be written in, you need Envoy in front of each one, which is usually already true if you’re running a service mesh.
Service mesh authorization policies
Istio and Linkerd, the two dominant service meshes, both build authorization directly into the mesh’s control plane rather than requiring every team to wire up ext_authz by hand. Istio’s AuthorizationPolicy custom resource lets platform teams declare coarse, perimeter-and-module-layer rules — which services may call which other services, over which paths and methods — enforced by the same Envoy sidecars the mesh already injects for mTLS and traffic management. This is deliberately the coarse end of the granularity spectrum: service-to-service and role-shaped decisions, not per-resource ones. A mesh AuthorizationPolicy answering “can the billing service call the ledger service’s /write endpoint at all” and an OPA sidecar answering “can this specific request write to this specific ledger entry” are two different granularity layers, both decentralized, both running in the same sidecar infrastructure — the mesh’s own policy engine typically handles the coarse layer, with OPA (or an equivalent) plugged in via ext_authz for anything that needs to go finer.
Authorization at the edge
Push the sidecar idea one step further, past the boundary of your own infrastructure entirely, and you get edge authorization: running a decision inside a CDN’s own request-handling runtime — Cloudflare Workers, AWS CloudFront Functions or Lambda@Edge, Fastly Compute — before the request has traveled any further than the nearest point of presence to the user.
The appeal is latency at a scale sidecars can’t touch: a user in Singapore hitting an edge location in Singapore gets a reject decision in single-digit milliseconds, without their request ever crossing an ocean to reach your origin region at all, for traffic that was never going to be allowed in the first place (an expired token, a malformed request, a blocked IP range). This matters disproportionately for the perimeter layer from the granularity spectrum — precisely the layer that’s supposed to reject cheaply and fast, and there is no faster place to reject than before the request has even left the user’s continent.
The limits are just as real, and they’re structural, not a maturity gap that will close over time. Edge runtimes are deliberately constrained: short CPU-time budgets (they’re billed and scheduled for thousands of tiny, cheap invocations, not one expensive one), limited or no direct access to your internal databases, and no reasonable way to hold a warm, actively-synced copy of a large relationship graph. An edge worker can verify a JWT’s signature and check its claims — that’s a self-contained, fast, perimeter-layer operation. It generally cannot run a Zanzibar-style Check that needs to traverse a relationship graph living in your origin region’s database; the round-trip to fetch that data would erase the latency advantage that made running at the edge worth doing in the first place.
WASM: the portability layer underneath all of this
A sidecar, an edge worker, and an application process embedding a policy library are three runtimes with almost nothing in common — different languages, different CPU and memory constraints, different deployment mechanisms. What lets the same policy run correctly in all three without being rewritten three times is WebAssembly (WASM): a portable, sandboxed bytecode format that every one of these runtimes can execute.
OPA compiles a Rego policy into a .wasm module (opa build -t wasm), and that single artifact is what gets loaded into a sidecar’s OPA process, embedded directly into an application via OPA’s WASM SDKs (Go, JavaScript, and others), or run inside an edge worker — Cloudflare Workers, in particular, has first-class WASM support specifically because its runtime already needed a fast, sandboxed execution model for untrusted, CPU-metered code, and a compiled policy fits that shape naturally. Cedar has moved in the same direction, with its own reference implementation designed to be embeddable, and the broader trend across every engine in this module is the same: compile the policy once, run the compiled artifact anywhere, instead of shipping a full language runtime (a Rego interpreter, say) to every environment that needs a decision.
This matters for decentralization specifically because it turns “run policy at the edge” from “port your policy engine to a constrained new runtime” into “load the same artifact you already build for your sidecars into a WASM host the CDN already provides” — the portability is what makes edge authorization for anything beyond hand-written token checks practical at all.
Policy distribution: staying consistent without a central decider
Every pattern in this article shares one problem the sidecar callout above named: if no single process makes every decision, how do potentially thousands of independent deciders — sidecars across a fleet, edge workers across dozens of points of presence — agree on what the current policy actually is?
The answer decentralized architectures converge on is separating authoring from evaluation, which is really the PAP-from-PDP separation the XACML article established, now stretched across a much larger number of PDP copies. A Policy Administration Point remains the single source of truth — the place policy is written, reviewed, tested, and versioned — but instead of every decision calling it live, it publishes immutable, versioned bundles, and every sidecar and edge worker independently pulls (polling on an interval) or receives (via push, for lower-latency rollout) the latest bundle and evaluates entirely against its local copy.
flowchart LR accTitle: Policy bundle distribution from a central PAP to a decentralized fleet of sidecars and edge workers accDescr: The diagram shows a policy author committing a change, which passes through CI and gets published by the policy administration point as a new signed bundle to a bundle registry. From the registry, two separate groups pull the bundle independently and asynchronously, out of the request path. The first group is a fleet of sidecars, each polling the registry on its own interval and loading the new bundle into local memory. The second group is a set of edge workers at different points of presence, each also pulling and loading the bundle independently. A note states that request-time evaluation at every sidecar and edge worker uses only the locally loaded bundle, never calling the registry or the policy administration point live. Author[Policy author] --> CI[CI: test and sign] CI --> PAP[PAP publishes bundle] PAP --> Registry[(Bundle registry)] Registry -.pull.-> S1[Sidecar fleet<br/>polls on interval] Registry -.pull.-> S2[Edge workers<br/>polls on interval] S1 --> Local1[local bundle,<br/>evaluated offline] S2 --> Local2[local bundle,<br/>evaluated offline]
This is a deliberate trade: request-time evaluation no longer depends on the registry or the PAP being reachable at all — a sidecar or edge worker with a locally loaded bundle keeps making correct decisions against that version of policy even during a network partition, which is a real resilience win. What’s given up is instantaneous consistency: there is now an unavoidable propagation window between “policy changed at the PAP” and “every decider in the fleet is using it,” bounded by each poller’s interval (or push latency), during which different deciders can correctly and simultaneously be evaluating two different versions of policy. Decision logs — every sidecar and edge worker shipping its individual decisions back to a central log store even though the decision itself was made locally — are how most deployments recover the visibility a central PDP would have given for free: the decision is decentralized, but the audit trail deliberately isn’t.
Multi-region and multi-cloud: when the data isn’t in one place either
Bundle distribution solves consistency for policy — the rules. It does not solve consistency for the data those rules evaluate against, and that problem gets sharply harder once relationship or attribute data itself is replicated across regions or clouds rather than living in one database.
Revisit the new enemy problem from the fine-grained authorization article with this lens. A single-region Zanzibar-style deployment can pin a Check to a zookie because the write that revoked access and the check reading that data share, realistically, one consistent store. Once that relationship data is replicated across, say, a US region and an EU region — for latency (serve EU users from EU infrastructure) or for data residency requirements (GDPR-driven obligations to keep EU personal data, which relationship tuples about EU users typically are, within the EU) — a revocation written in one region has to physically propagate to the other before a check served there can see it. That propagation has a floor set by the speed of light and network topology, not by better software, and no amount of engineering makes it instantaneous.
sequenceDiagram accTitle: The new enemy problem across regions, where revocation must physically propagate over a longer link accDescr: The diagram shows an admin revoking Sara's access in the EU region's data store at time T. That change begins replicating toward the US region's data store over a cross-region link, which takes measurable time bound by network topology. Shortly after time T, a check for Sara's access arrives at the US region, which is closer to the requester, and reads from the US region's replica. Because the EU revocation has not yet arrived over the cross-region link, the US replica still shows Sara with access, and the check incorrectly returns Allow, illustrating that the new enemy problem persists across regions with a propagation delay set by physical distance rather than same-datacenter replication lag. Admin->>EU_DB: revoke Sara's access (T) EU_DB-->>US_DB: replication in flight Client->>US_Engine: Check(sara, viewer, doc) US_Engine->>US_DB: read local replica US_DB-->>US_Engine: still has access (stale) US_Engine-->>Client: Allow (wrong)
There is no engineering trick that makes this trade-off disappear — only a deliberate choice, made per region and per class of decision, about which side of it you accept. Three real options: pay the cross-region write latency on every sensitive change by requiring revocations to be acknowledged in every region before the initiating request completes (strongest consistency, worst write latency); accept a bounded staleness window and document it as an explicit SLA (the SpiceDB-style at_least_as_fresh versus minimize_latency choice from the fine-grained authorization article, now made per-region rather than per-request); or partition the data so that sensitive relationships never need cross-region reads at all — an EU user’s access to EU-hosted resources is decided entirely within the EU region, and the cross-region case simply doesn’t arise for that class of data. Most real multi-region deployments use a mix of the third and second options: partition what can be partitioned, and accept a small, monitored, explicitly-documented staleness window for the rest.
Worked example: one cashback app, deployed globally
The cashback company from earlier articles expands into the EU and needs to decide, concretely, where each of its authorization decisions runs. Perimeter checks — is this request’s token valid at all — run at the edge, globally, in Cloudflare Workers compiled from the same OPA WASM bundle the company already builds for its sidecars; an invalid or expired token is rejected in the point of presence closest to the user, in milliseconds, without ever reaching either region’s origin. Module-layer checks — does this user’s role permit opening the merchant-dashboard feature at all — run in Istio AuthorizationPolicy rules enforced by the mesh’s own Envoy sidecars, service-to-service and role-shaped, decentralized across every pod in both regions identically because the mesh configuration itself is bundle-distributed the same way OPA’s policy is. Resource-layer checks — can this specific merchant edit this specific transaction record — run in an OPA sidecar per service, evaluating against attribute data replicated per-region, with EU merchant data partitioned to stay entirely within the EU region so no cross-region read is ever needed for this class of decision. Relationship-layer checks — can a merchant’s teammate view a cashback report shared with their team — run against an OpenFGA deployment that is region-aware: EU teams’ relationship tuples live in the EU region’s store, US teams’ in the US store, and the rare cross-region sharing case (a global admin account, say) explicitly accepts the at_least_as_fresh latency cost rather than the fully_consistent one, documented as a known, bounded exception rather than a silent gap.
Four granularity layers from the previous article, now each placed deliberately at the topology layer that fits it — edge for perimeter, mesh sidecars for module, service-local sidecars for resource, a region-aware relationship store for relationship — which is the whole point this article has been building to: granularity and placement are two separate decisions, and a mature authorization architecture makes both, for every feature, instead of assuming one placement serves every layer.
Testing and operating a decentralized deployment
Testing a decentralized authorization system needs a check the earlier articles’ single-PDP testing story didn’t: bundle rollout verification — deploying a new policy bundle to a canary slice of sidecars or edge locations first, confirming decisions match expectations there, before it reaches the full fleet, precisely because a bad policy bundle pushed everywhere simultaneously has no single point where you could have caught it before it took effect broadly. Most OPA deployments pair this with decision logging shipped centrally from every sidecar and edge worker, which is how a decentralized fleet gets back the single-place-to-audit property a central PDP has by default.
Operationally, the metric worth watching is bundle staleness: how far behind the latest published version is each sidecar and edge worker actually running, right now, across the fleet — because that gap is exactly the propagation window during which different deciders can be correctly, simultaneously evaluating different policy versions. A fleet where that gap is consistently small and monitored is a decentralized system behaving as designed; a fleet where it’s silently growing (a poller stuck, a push mechanism failing for one CDN region) is a decentralized system quietly drifting out of sync with itself, which is the specific failure mode this whole architecture pattern needs to watch for that a single central PDP never had to.
Recap
Placement is the layer above every other authorization decision in this module:
- The PDP’s location changes what it can promise, not just how fast it responds — library, sidecar, central service, edge, and multi-region each trade latency, consistency, and blast radius differently.
- Sidecars decentralize evaluation to every service instance, typically via Envoy’s
ext_authzcalling a local OPA, keeping most of a library’s latency benefit without coupling policy to application code — at the cost of a policy-distribution problem across every copy. - Edge authorization pushes the perimeter layer as close to the user as physically possible, but is structurally limited to what a constrained, data-poor runtime can decide — it’s a filter in front of your real authorization stack, not a replacement for it.
- WASM is the portability layer that lets one compiled policy artifact run in a sidecar, an edge worker, and an embedded library without being rewritten per runtime.
- Policy distribution solves consistency for rules via versioned bundles, trading instantaneous propagation for availability — the same PAP/PDP separation from the XACML article, now stretched across thousands of PDP copies.
- Multi-region and multi-cloud make the new enemy problem structural, bound by physical propagation delay rather than software quality — solved by a deliberate, documented, per-region consistency policy, not engineered away.
Three questions to test yourself
- Explain why a sidecar’s latency profile is close to a library’s even though it involves a second process — what specifically makes a
localhostcall to anext_authzsidecar different from a call to a central PDP service over the network? - A team wants to run their full Zanzibar-style relationship-graph Check directly inside a CDN edge worker, to get the lowest possible latency. Explain, using this article’s constraints on edge runtimes, why this generally doesn’t work and what should run at the edge instead.
- Describe, in your own words, why policy bundle distribution is really the same PAP/PDP separation from the XACML article rather than a new idea — and explain what specifically is traded away (compared to a single central PDP) to gain the resilience of a decentralized fleet.
Hands-on exercises
- Design a sidecar deployment. For a service you’re familiar with (or a hypothetical one), sketch what an Envoy + OPA sidecar deployment would look like: what does the
ext_authzcall check, what policy does the local OPA instance need loaded, and what happens to a request if the sidecar itself is momentarily unavailable. - Classify a request path by topology. Take one user-facing action in an application you know (placing an order, viewing a dashboard, editing a shared document) and, using this article’s four topology layers (edge, mesh sidecar, service sidecar, region-aware data store), assign each authorization decision in that request’s path to the layer where it should actually run, and justify each choice.
- Design a consistency policy for one cross-region case. Pick one sensitive authorization decision that could plausibly need to work correctly across two regions (a revoked account, a cross-region admin role). Choose one of the three options this article describes (pay cross-region write latency, accept and document a bounded staleness window, or partition the data to avoid the cross-region case entirely) and justify the choice against the decision’s actual sensitivity.
Frequently asked questions
What does 'decentralized authorization' mean here, and how is it different from what the XACML article called a 'distributed PDP'?
The XACML article named three PDP placements — library, sidecar, central service — as a trade-off it left open. This article is that trade-off developed in full, plus two placements XACML's era didn't really have: the network edge, and multi-region/multi-cloud deployment. 'Decentralized' here specifically means no single PDP instance sees every request; decisions are made close to where the request already is, by many cooperating instances that share policy and (sometimes) data rather than by one central decider every request must reach.
What is a sidecar authorization pattern, and why put OPA next to every service instance instead of calling one central service?
A sidecar is a second process deployed alongside every instance of your application (typically in the same Kubernetes pod), and in the authorization context it's usually an OPA instance an Envoy proxy calls over localhost via the ext_authz filter. The reason to do this instead of calling a central service is latency and blast radius: a localhost call has no network hop and no dependency on a remote service being reachable, so authorization keeps working even if the rest of the cluster is having a bad day. The cost is that you now have one OPA instance per pod instead of one service, which is a policy-distribution problem, not a policy-evaluation problem.
How does authorization work at the edge (CDN), and what are its limits?
Edge authorization runs a decision inside a CDN's request-handling runtime (Cloudflare Workers, AWS CloudFront Functions/Lambda@Edge, Fastly Compute) before the request ever reaches your origin infrastructure — typically token verification and coarse role/claim checks, because edge runtimes are deliberately constrained (short CPU budgets, limited or no ability to reach your internal databases or relationship stores). Edge is the perimeter layer from the granularity spectrum, running as close to the user as physically possible; it structurally cannot do a fine-grained relationship-graph check, and shouldn't try — it exists to reject the traffic that never needed to reach your origin at all.
Why does WASM matter for decentralized authorization?
WebAssembly is what lets the same compiled policy run unmodified in a sidecar, inside a CDN edge worker, and embedded directly in an application process — three runtimes with very different constraints, unified by one portable bytecode target. OPA compiles Rego policy to a WASM module specifically so 'write the policy once' doesn't turn into 'rewrite the policy once per place it needs to run,' which would otherwise be the single biggest obstacle to decentralizing an authorization engine across sidecar, edge, and in-process deployments consistently.
How do you keep policy consistent across many sidecars and edge locations if there's no central decider?
By separating who authors policy from who evaluates it: a central Policy Administration Point still exists and is still the single source of truth, but instead of every request calling it live, it publishes versioned policy bundles that every sidecar and edge worker pulls (or is pushed) on its own schedule, then evaluates entirely locally. This trades perfect real-time consistency (every decider sees every policy change instantly) for availability and latency (deciders keep working, correctly, using the last bundle they successfully pulled, even if the network to the PAP is down) — the same kind of trade-off the fine-grained authorization article made about data consistency, now applied to policy itself.
What changes about the new enemy problem when authorization is decentralized across regions?
It gets structurally harder, not just slower. In a single-region Zanzibar-style deployment, a zookie pins a check to a snapshot no older than a given write, and that write and that check can reasonably share a consistent view of the same data store. Once relationship data is replicated across regions, propagating a revocation with the same freshness guarantee means either paying cross-region write latency on every sensitive change, or accepting that a check served in a different region than the revocation was made in has a real, physical-speed-of-light-bounded window where staleness is possible — the trade-off no longer disappears with better engineering, it becomes a deliberate per-region consistency policy you have to choose and document.