Finding the PDP
How an enforcement point works out which PDP decides a request, how several PDPs and their information points combine behind one API, and how the answer moves from operator configuration to the API's own document to what a federation vouches for.
The resolver
The resolver answers one question for each layer of a route's policy: given the protected resource this request is for, where is the AuthZEN evaluation endpoint? It answers in two hops. First it finds the PDP identifier, from operator configuration, from the resource's RFC 9728 document, or from the federation's resolved metadata for the resource. Then it reads that PDP's /.well-known/authzen-configuration for its endpoints, and uses the spec's default paths when the PDP publishes none.
Both hops are cached per identifier. On top of them, a route names an ordered list of layers:
resourcemeans whatever discovery finds for the route's resource. It is the default.staticis the configuredAUTHZEN_URL, whatever discovery finds. It is the slot for an estate-wide PDP.- A PDP identifier is an explicit PDP, read like any other and checked against
PDP_ALLOWLIST.
The resource's document can name more than one PDP. Its authzen_policy_set lists the PDPs to ask and how their answers combine. That set is what the resource layer means, and layers configured on the PEP go in front of it. A document without a set publishes nothing about who decides, and the static PDP answers for it.
One request, end to end
The sequence below follows a tool call to the demo's bank-b API through coaz-pep, with nothing set on the route. Bank B's document names its own PDP and puts the estate PDP in front of it. The estate PDP permits. Bank B's PDP denies with step-up advice, and the agent gets a challenge it can act on.
WWW-Authenticate: … insufficient_scope on REST, and as data.authz_challenge.type = resource_authorisation in a JSON-RPC error on MCP.What the sequence shows
- A PDP that can't be resolved stops the request before any PDP is asked, unless it is allowed to fail open. The PEP never runs part of a policy without saying so.
- The resource allowlist is checked before any cache or fetch. A refused resource costs nothing and can't pick up another caller's cached answer.
- Every PDP the document names passes the same
PDP_ALLOWLISTas one named in configuration, however deeply it is nested in the set. - Every member gets the same request and context. In a
deny-overridesset the first deny is the answer, advice included, and later members aren't asked. Obligations from the members that permitted carry forward. - On a warm cache, steps 5 to 8 and 11 make no network calls. Both caches serve the last good answer for a bounded time if a fetch fails.
Several PDPs, and the PIPs behind them
The PEP's job ends at the PDP call. Policy information points (PIPs) belong to the PDPs: a PDP that wants risk scores, entitlements or consent fetches them itself. The PEP adds one thing every PDP can use. It forwards the resource's metadata document verbatim in context.resource_metadata, and resource_metadata_source says whether that document was self-asserted (rfc9728) or vouched for (federation).
AuthZEN PDPs behind one engine
PDP_ALLOWLIST, whatever a document says. The bank PIPs are examples. Without coaz-pep, Kong and PingAccess decide REST routes natively with the same discovery. Every MCP request still goes to coaz-pep, so that one parser reads every MCP body.A policy set the API publishes
An authzen_policy_set is a tree. A member is a PDP identifier, a PDP with options, or a nested set. A set combines its members' answers with deny-overrides (every member must permit), permit-overrides (the first permit decides) or first-applicable (the first answer decides). Members are asked in document order and asking stops as soon as the answer is known, so every PEP makes the same calls in the same order.
fail_open says. A set is limited to 4 levels of nesting, 8 members per set and 16 distinct PDPs, and every PDP in it passes the allowlist before anything is asked.Whose word counts on failing open. A member without fail_open takes its set's setting, and at the top the PEP's failure mode, which is closed unless the operator chose otherwise. In federation mode a published fail_open: true counts unless a superior pins authzen_fail_open_permitted to false. In RFC 9728 mode the document is self-asserted, so a published fail_open: true is ignored unless the operator allows it. A published fail_open: false always counts, because it can only close.
Three generic PingAuthorize PDPs over Sideband
With the sideband-pdp Kong plugin, the PDPs are PingAuthorize engines, each running one generic policy: token, risk or response. None of the policies contains an API, path, scope or account. Everything specific comes from the API's RFC 9728 document, which every policy reads as a PIP. The same document publishes the three engines as its policy set.
deny-overrides over the token, risk and response PDPs. Each engine validates the access token against the issuer's JWKS, so the policies only read verified claims. The token and risk PDPs are request-only on the route. The response PDP is asked twice, once for the request and once for the API's response, which it filters and masks using answers from the entitlements PIP (authzen_entitlements_pip). The risk engine's URL is operator configuration and never comes from the document. The details are in the resource policy design note.Resolution modes
The mode decides where the first hop gets its answer: PDP_DISCOVERY on coaz-pep, and pdp_discovery on the Kong plugins. Read the rows from top to bottom as moving the source of truth further from the PEP. The second hop, from PDP identifier to endpoints, is the same in every AuthZEN mode.
coaz-pep. Kong, PingAccess and the Node SDK implement resource and authzen natively, and get federation by delegating to coaz-pep.What a lookup can come back with
A metadata lookup has four possible outcomes, and the resolver keeps them apart on purpose. The important split is between "publishes nothing" and "can't be reached". If an outage fell through to the static PDP, anyone who could take an API's metadata offline could choose a different judge for it.
X-PDP-Fail-Open.Up to federation
An API's own document can't vouch for itself. A careless or compromised API can name a PDP that permits everything, which is what the demo's impostor does. In federation mode the PEP doesn't read the API's document at all. It walks a Trust Chain from the API up to an anchor whose keys the operator configured, and uses the metadata that survives every superior's metadata_policy.
Walking the chain
{R}/.well-known/openid-federation, a suffix rather than an insertion after the host. The walk follows authority_hints breadth-first and tracks loops per path, so an intermediate reachable two ways can't hide the chain to the preferred anchor. The resolved answer expires at the earliest exp in the chain.Once the federation vouches for an API, its resolved metadata is the only word that counts. If that metadata has no oauth_resource block, or has no usable authzen_policy_set, the result is a refusal, not a fallback to the static PDP. A fallback would quietly drop the PDPs and the acr floor the anchor set.
What the policy does to a member's metadata
The demo's member shows the effect. Its own Entity Configuration's set names the rogue PDP first and says a password is enough. The anchor's policy replaces both: it pins a set with the estate PDP first, then Bank A's.
| Parameter | Member's Entity Configuration | Anchor's metadata_policy | Resolved, forwarded to the PDP |
|---|---|---|---|
authzen_policy_set |
deny-overrides [rogue-pdp, pdp-a] |
value: deny-overrides [pdp-estate, pdp-a], essential: true |
deny-overrides [pdp-estate, pdp-a] |
acr_values_required |
password |
value: [MFA] |
[MFA] |
essential makes a resolved document without a set invalid. The PEP forwards the resolved document with resource_metadata_source: federation, so the PDP sees MFA as the requirement and denies a password token. A set is a nested structure, so value, which pins it whole, is the only standard operator that applies to it.The PEP as the API's federation face
An API in a federation has to publish an Entity Configuration and its RFC 9728 metadata. The gateway is the API's public face, so coaz-pep can publish both. The PEP holds a key and an entity identifier. The trust anchor, acting as controller, holds the API's policy.
authority_hints and {"resource": id}. Policy changes go in the anchor's Subordinate Statement and reach every consumer on its next refresh, with no PEP redeploy. Until the anchor onboards the PEP, its document is self-asserted and names only the configured PDP, and the header says so. Kong and PingAccess relay both documents from coaz-pep. The Node SDK signs its own, which is always self-asserted.Rules that hold in every mode
AUTHZEN_URL. A discovered PDP never receives it.access_evaluations_endpoint.More detail: the one-page overview, the resource policy design note, the demo guide and the architecture. Try each mode in the hosted demo.