Every service has to decide who may do what, and the choice between asking a central policy engine and checking inside the code shapes latency, outage behaviour, audit quality and how fast rules can change for years afterwards. Embedded authorisation puts the decision logic in the application itself, as role checks, ownership queries and framework annotations. Centralised (or externalised) authorisation moves the decision to a policy decision point (PDP) that the application, gateway or sidecar asks through a policy enforcement point (PEP). Most estates end up with a mix. This guide is for architects, platform teams and application security leads making that call. It goes deeper than the Policy-as-Code for Authorisation Guide, which covers writing and shipping policies, and the Externalised Authorisation Buyer's Guide, which covers choosing a product.
Key takeaways
- Centralise the policy, not necessarily the network hop. A policy can be authored, tested and versioned in one place and still be evaluated locally, as a sidecar, a library or compiled WebAssembly, which removes most of the latency and availability argument against it.
- Embedded checks fail silently, central checks fail loudly. A missing in-code check exposes data without any error; an unreachable PDP denies everything if the PEP fails closed. Plan for the loud failure, because it is the safer one.
- Drift is the real cost of embedding. When the same rule lives in ten services, written by ten teams, the copies diverge, and nobody can answer "who can approve a refund?" without reading code.
- Decision logs are the audit dividend. A PDP can record the input, the policy version and the result for every decision; embedded code rarely records more than an HTTP 403.
- Migrate one decision at a time, in shadow mode. Run the PDP alongside the existing check, compare results, fix mismatches, then switch enforcement, keeping the old path ready for rollback.
What each approach actually means
The terms are used loosely, so it helps to separate three things that are often bundled together: where the policy is written, where it is evaluated and where it is enforced.
- Embedded authorisation: the rule is written in the application's own language, evaluated in the same process and enforced by the same code. Examples are an
if user.role == "admin"check, a framework annotation on a controller method, or a SQLWHERE owner_id = ?clause that doubles as an access rule. - Centralised authorisation: the rule is written in a policy language (Rego, Cedar or a vendor format) and managed in one repository or policy administration point. A PDP evaluates it, and the application, API gateway, service mesh or agent tool server enforces the answer as a PEP.
- The hybrid most teams reach: policy authored centrally, distributed as bundles and evaluated close to the caller. It is still centralised where governance matters: one source of truth, one test suite, one log format.
The split between decision and enforcement is the core idea. The application asks "may this subject perform this action on this resource in this context?" and acts on the answer. The OpenID AuthZEN Authorization API Guide covers the standard that now defines that question on the wire: the OpenID Foundation published Authorization API 1.0 as a Final specification on 11 January 2026, with subject, resource, action and context as its four request elements.
Side-by-side comparison
| Dimension | Embedded (in-app checks) | Centralised (PDP and PEP) |
|---|---|---|
| Where rules live | Spread across services, languages and frameworks | One policy repository, one language, reviewed like code |
| Decision latency | In-process, effectively zero added network time | Library or Wasm: in-process; sidecar: local loopback call; remote service: a network round trip per decision |
| Availability dependency | None beyond the application and its database | The PDP, its policy bundles and its data sources become part of the request path |
| Consistency across services | Depends on every team implementing the rule the same way | Same policy version evaluated everywhere, subject to bundle propagation delay |
| Change speed | A rule change is a code release per service | A rule change is a policy release, tested once, rolled out to all PDPs |
| Audit and explainability | Usually an access-denied log line with no reason | Decision logs with input, result, policy revision and decision ID |
| Data access | Direct access to the application's own data model | Needs attributes passed in the request or synchronised to the PDP |
| Failure when wrong | Silent over-permission on a missed check | Visible denial on outage if fail-closed; broad exposure if a shared policy is wrong |
Threat model: what each approach defends against
Broken access control is the commonest serious application flaw, and the two approaches fail against different parts of it.
- Missing checks: the classic embedded failure. A new endpoint ships without the ownership check, and objects become readable by any authenticated user, the pattern behind OWASP API Top 10 API1 Broken Object Level Authorization Guide. Centralisation helps only if the PEP is wired in by default (at a gateway, mesh or framework middleware) so that a new route is denied until a policy covers it.
- Inconsistent checks: the web app enforces a rule that the mobile API or batch job does not. Centralised policy removes the second copy; embedded estates need shared libraries and tests to approximate it.
- Function-level gaps: admin functions reachable by ordinary users, covered in the OWASP API Top 10 API5 Broken Function Level Authorization Guide. A gateway PEP can block whole route groups regardless of application code.
- PDP and policy supply chain: new in the centralised model. Whoever can change policy bundles, the PDP's data or the PEP-to-PDP channel can grant access everywhere at once. The AuthZEN specification requires the PEP-to-PDP connection to be protected for integrity and confidentiality, typically with TLS, and says the PDP should authenticate the calling PEP, recommending OAuth 2.0.
- Bypass of the enforcement point: traffic that reaches the service without passing the PEP. Envoy's documentation for its external authorisation filter warns that per-route configuration combined with later filters that clear the route cache can let requests bypass authorisation checks, a reminder that the PEP must sit where it cannot be routed around.
Architecture in words: PDP and PEP placement
Placement decides latency, blast radius and who owns the outage. Four patterns cover almost every deployment.
Library or in-process evaluation
The policy engine runs inside the service. The Open Policy Agent (OPA) integration documentation lists a Go SDK and a lower-level Go API for embedding OPA as a library, and compilation of Rego policies to WebAssembly so they can be evaluated by any Wasm runtime, rating evaluation as fast for the REST API, faster for the Go library and fastest for Wasm because communication stays inside one operating-system process. This keeps central authoring while removing the network hop; the cost is that every service must load policy updates and ship decision logs itself.
Sidecar or host-level daemon
The PDP runs next to each service on the same host or pod and is called over loopback. OPA's documentation states that running it on the same host helps ensure policy decisions are fast and highly available, and notes that the separate process needs its own monitoring and logging.
Gateway or mesh enforcement
The PEP moves out of the application into an API gateway or service mesh proxy, which calls the PDP before forwarding the request. Envoy's ext_authz filter, for example, calls an external gRPC or HTTP authorisation service and returns HTTP 403 when the request is not authorised; its documented gRPC example notes a 200 ms default timeout. It suits route and tenant rules but sees only the request, so it cannot judge record ownership without a lookup. See the API Gateway Security Guide.
Remote shared PDP service
One PDP cluster serves many applications over the network. It suits relationship-based systems needing a shared graph. Google's Zanzibar paper describes a global system of this kind handling millions of authorisation requests per second with 95th percentile latency under 10 milliseconds and availability above 99.999% over three years; those figures reflect heavy engineering, not a first deployment. For the relationship model itself, see the Fine-Grained Authorisation Guide.
Latency and availability failure modes
An authorisation call sits on the critical path of every request, so its budget is small. OPA's performance documentation gives the example that a microservice API authorisation decision may have a budget in the order of one millisecond.
| Failure mode | Embedded effect | Centralised effect | Control |
|---|---|---|---|
| PDP unreachable or timing out | Not applicable | All decisions fail; requests denied if fail-closed, allowed if fail-open | Fail closed by default; co-locate PDPs; set explicit timeouts; alert on timeout rate |
| Fail-open misconfiguration | Not applicable | Outage silently becomes allow-all | Keep Envoy failure_mode_allow false; monitor the failure_mode_allowed statistic; test PDP outage in pre-production |
| Stale policy bundle | Not applicable | Revoked rule still enforced on some nodes | Monitor bundle revision per PDP; alert on lag; include revision in decision logs |
| Slow policy | Slow code path in one service | Every caller slows | Benchmark with opa bench and opa eval --profile in CI; use indexable rule patterns |
| Attribute source outage | Application database outage | PDP cannot fetch roles or relationships | Cache attributes with bounded staleness; pass stable attributes in the token or request |
| Memory pressure | Rare | PDP killed under load | OPA notes raw JSON data uses roughly 20 times more memory once loaded; size data and set GOMEMLIMIT below the container limit |
AuthZEN states that evaluation decisions default to closed (false) and treats a 200 response with a false decision as a denial distinct from an HTTP error. It does not define what a PEP should do when the PDP cannot be reached, so write that behaviour into your own standard: deny, return a clear error, log it and alert. Fail-open should be a documented, time-limited exception for a named low-risk route, never a global default.
Policy drift, audit and explainability
Drift in embedded estates builds up because each team re-implements shared rules: one service treats a suspended account as read-only, another as fully blocked. Counter it with a shared library, one action and resource list, and contract tests in every service.
Drift in centralised estates is different: the policy is single-sourced, but PDPs can run different revisions while bundles propagate, and attribute data can diverge from the system of record. Measure revision skew rather than assuming it is zero.
Explainability is where centralisation pays off most. OPA decision logs can carry a unique decision_id, the revision of each bundle used, the policy path, the input and result, and W3C trace and span IDs, with a masking policy (data.system.log.mask) to erase or modify sensitive input fields before upload. Cedar returns the determining policies with each answer: the satisfied permit policies on Allow, or the satisfied forbid policies on a forbid-driven Deny. That lets an auditor answer "why was this allowed?" with a policy ID rather than a code walk. Two cautions: OPA drops decision log events when max_decisions_per_second is exceeded, and Cedar skips a policy that errors rather than failing the whole request, reporting the error in diagnostics. Monitoring needs to see both.
Service-to-service and agent identity in decisions
Centralised policy makes it practical to decide on more than one identity at once, which matters most for workloads and AI agents.
- Workload callers: the subject is a service, identified by a workload identity such as a SPIFFE ID or cloud-issued token, not a user. Policies should name the calling workload and the action, not just "internal network". See the Cloud Workload Identity Guide.
- Delegated calls: when service A calls service B on behalf of a user, the decision should see both: the user who started the request and the service carrying it. Token exchange preserves that chain rather than passing the user's token through; see the OAuth Token Exchange and Delegation Guide.
- AI agents: an agent acting for a user should never exceed that user's rights, and each tool call should be decided in context. Embedded checks in every tool server make that rule hard to apply consistently; a shared PDP evaluating agent, user, tool and resource together makes it one policy. See the AI Agent Authorisation Guide.
- The PEP's own credential: the PEP is itself a non-human identity calling the PDP. Give it a dedicated, short-lived credential and restrict which PDP endpoints it may call.
Decision criteria: which to choose where
| Situation | Lean towards | Why |
|---|---|---|
| One application, one team, simple roles | Embedded, with a shared middleware layer | A PDP adds operations without enough reuse to repay it |
| Same rule enforced in many services or languages | Centralised | Removes duplicate implementations and drift |
| Auditors or regulators ask why access was granted | Centralised | Decision logs and determining policies give per-decision evidence |
| Rules depend heavily on the application's own data (ownership, workflow state) | Hybrid: data lookup in the app, decision in the PDP; or embedded with tests | Shipping all that data to a PDP may cost more than it saves |
| Sub-millisecond, very high-volume paths | In-process library or Wasm evaluation of central policy | Keeps governance without a network hop |
| Sharing, folders and nested tenants | Central relationship-based service | Graph traversal needs a shared store |
| AI agents calling many tools | Centralised | One policy can bind agent, user and tool consistently |
Small vs large organisations: a small team gains most from deny-by-default middleware and a single authorisation module; a large estate gains most from a central policy repository, local evaluation and a standard PEP contract such as AuthZEN so PDPs can change without touching every application. The Authorisation Maturity Model helps place each application tier before choosing.
Worked scenario: refund approvals across five services
A retailer has a rule: support agents may approve refunds up to 200 pounds, team leads up to 2,000 pounds, and nobody may approve a refund on their own order. The rule is embedded in the web console, the mobile support app, the bulk-refund job, the chatbot back end and a new AI assistant that drafts refunds.
- Embedded today: the bulk job checks the limit but not the own-order rule; the chatbot uses an older 500 pound limit; the assistant calls the refunds API with a service account that has no limit at all. Each gap is silent. An auditor asking why an agent approved 1,800 pounds finds only a log line saying "approved".
- Centralised target: one Cedar or Rego policy defines the limits and a forbid rule for own orders. The refunds API is the PEP for every channel. The assistant calls with an exchanged token that carries both the assistant's identity and the supervising agent, so the policy applies the agent's limit. Each decision log records the policy revision, the amount and the determining rule.
- Trade-off accepted: the refunds API now depends on a local PDP sidecar. The team sets a 50 ms timeout, fails closed, and alerts when the timeout rate passes 0.1%. During a PDP outage, refunds queue rather than proceed.
Migration path: embedded to centralised
- Inventory decisions: list every authorisation check per service (action, resource type, rule, data used). Static analysis finds most; interviews find the rest.
- Define the vocabulary: agree resource types, action names and subject attributes once. Map them to the AuthZEN subject, resource, action and context model so the PEP contract is engine-neutral.
- Pick one high-value decision: one where drift or audit pain is already visible, such as refunds or tenant admin, not the busiest read path.
- Write and test the policy: unit tests for every allowed and denied case, plus a forbid or default-deny test. Benchmark it against the latency budget.
- Run in shadow mode: the existing in-code check still enforces; the PEP also asks the PDP and logs both answers. Investigate every mismatch: each is either a policy bug or an existing code bug.
- Switch enforcement on a pilot ring: one tenant, region or percentage of traffic, with the in-code check kept as a feature-flagged fallback.
- Roll back cleanly: the rollback is a flag flip back to the in-code path, rehearsed before go-live. Keep it until the PDP has met its error and latency targets for an agreed period.
- Remove the old check: delete the embedded logic once the PDP is authoritative, so two sources of truth do not linger. Repeat for the next decision.
Detection and measurement
- Coverage: share of routes and tool calls passing through a PEP; target 100% for external routes.
- Decision latency: p99 PDP evaluation time and end-to-end authorisation time against the budget.
- PDP error and timeout rate: and any fail-open allows, which should be zero outside documented exceptions.
- Bundle revision skew: maximum time between policy publish and the last PDP running it.
- Shadow mismatch rate: during migration, the share of decisions where PDP and code disagree, trending to zero.
- Decision log completeness: logged decisions against requests served, catching rate-limit drops.
Alert in the SIEM on denial spikes for one subject, unusual workloads allowed sensitive actions, and policy changes outside releases. Stolen valid credentials (ATT&CK T1078 Valid Accounts) pass authentication, so authorisation logs are often where misuse first shows.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | AC-3 Access Enforcement; AC-3(13) Attribute-based Access Control; AC-24 Access Control Decisions | Enforcement on every request; attribute rules; separating decision from enforcement |
| NIST SP 800-53 Rev. 5 | AC-6 Least Privilege; AU-3 Content of Audit Records; CM-3 Configuration Change Control | Scoped rules; decision log content; policy releases under change control |
| NIST CSF 2.0 | PR.AA-05; PR.PS-04; DE.CM-03 | Permissions defined in policy and enforced; logs for monitoring; activity monitoring |
| ISO/IEC 27001:2022 Annex A | 5.15 Access control; 8.3 Information access restriction; 8.15 Logging | Access rules defined and enforced; decision evidence retained |
| CIS Controls v8.1 | Control 6 Access Control Management; Control 8 Audit Log Management | Centrally managed access rules; collection of decision logs |
| OWASP | Authorization Cheat Sheet; API Top 10 2023 API1 and API5 | Deny by default, check every request, test authorisation logic |
Common failure patterns
- Fail-open "temporarily": a timeout switched to allow during an incident and never reverted, turning every later PDP outage into open access.
- Half-migrated decisions: the PDP enforces for the web channel while the batch job keeps its own check, so two rules drift again. Remove old checks once the PDP is authoritative.
- Gateway-only enforcement for object rules: the gateway checks the route but cannot see ownership, so object-level flaws remain. Object rules need a PEP in the service.
- Unprotected policy pipeline: anyone with repository write access can publish a permit-all policy. Require review, signed bundles and policy tests in CI; the Authorisation Testing Automation Guide covers the tests.
- Service accounts with no limit: agents and jobs call through privileged service accounts that bypass user-level rules, as in the scenario above.
Practitioner checklist
- Inventory every authorisation decision per service and mark which are duplicated across services.
- Set a deny-by-default PEP at the framework, gateway or mesh layer so new routes are blocked until covered.
- Decide placement per path: library or Wasm for hot paths, sidecar for most services, shared service for relationship graphs.
- Write the PDP-unreachable behaviour into your standard: fail closed, explicit timeout, alert.
- Adopt a standard PEP contract such as AuthZEN to avoid engine lock-in.
- Put policy under version control, review, automated tests and latency benchmarks.
- Log decision ID, policy revision, subject, action, resource and result, with masking.
- Carry both the user and the workload or agent identity into decisions on delegated calls.
- Migrate one decision at a time through shadow mode, a pilot ring and a rehearsed rollback.
- Track coverage, p99 latency, error rate, revision skew and log completeness.
Standards and references
- OpenID Foundation: Authorization API 1.0
- NIST: SP 800-162 Guide to Attribute Based Access Control (ABAC) Definition and Considerations
- NIST: SP 800-207 Zero Trust Architecture
- OWASP: Authorization Cheat Sheet
- Open Policy Agent: Integrating OPA
- Open Policy Agent: Decision Logs
- Cedar: Authorization
- Google Research: Zanzibar, Google's Consistent, Global Authorization System
Related NHI Mgmt Group resources: Policy-as-Code for Authorisation Guide, OpenID AuthZEN Authorization API Guide, Authorisation Maturity Model, Authorisation Models Guide, Coarse-Grained vs Fine-Grained Authorisation.