The Kong sideband plugin
sideband-pdp enforces decisions from PingAuthorize's Sideband API. It sends requests to /sideband/request and upstream responses to /sideband/response. PDP discovery uses resource metadata or a federation resolver endpoint. The plugin shares discovery and policy-layer code with authzen-pdp. PingAuthorize policy controls request mapping and denial responses. Version 0.4.2 requires Kong Gateway 3.4 or later; 3.9 or later is recommended.
Install
DB-less: mount both plugin directories and enable this one. The discovery and contract modules are authzen-pdp's, required by name (kong.plugins.authzen-pdp.discovery, …contract), so that directory must be on the package path whether or not authzen-pdp is enabled. The rock, kong-plugin-sideband-pdp-0.4.2-1.all.rock on the release, depends on kong-plugin-authzen-pdp 0.4.2 for them: install that one first. On Kong's Enterprise images, apt-get install -y unzip before either: they ship without it, and luarocks needs it to open a rock.
# kong.conf, or environment
KONG_PLUGINS=bundled,sideband-pdp
KONG_LUA_PACKAGE_PATH=/opt/?.lua;;
KONG_LUA_SSL_TRUSTED_CERTIFICATE=system,/etc/kong/pingauthorize.pem # PingAuthorize's self-signed certificate
# compose
kong:
image: kong:3.9
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /kong/kong.yml
KONG_PLUGINS: bundled,sideband-pdp
KONG_LUA_PACKAGE_PATH: /opt/?.lua;;
KONG_PROXY_LISTEN: 0.0.0.0:8000
KONG_ADMIN_LISTEN: "off"
volumes:
- ./kong/kong.yml:/kong/kong.yml:ro
- ../gateways/kong/authzen-pdp:/opt/kong/plugins/authzen-pdp:ro # discovery.lua, contract.lua
- ../gateways/kong/sideband-pdp:/opt/kong/plugins/sideband-pdp:ro
ports:
- "8000:8000"
The plugin in Kong Manager
Kong Manager draws the form from schema.lua exactly as it does for authzen-pdp - every field below, defaults pre-filled, the required ones marked: service_url, shared_secret and secret_header_name. The Kong page shows what that looks like; this plugin has not been captured separately.
Every field
Fields are grouped by function. Types match the schema. ref marks a field that accepts a Kong vault reference such as {vault://env/paz-secret} instead of a literal, and enc one Kong encrypts at rest where it has a keyring. The first group is Ping's ping-auth plugin's configuration, names and defaults unchanged, so a route configured for that plugin ports over by changing the plugin name.
PingAuthorize and its secret
| Field | Type · default | Meaning |
|---|---|---|
service_url required · ref | string | PingAuthorize's base URL, without /sideband… in the path: /sideband/request and /sideband/response are appended. The static PDP. With discovery on, the PDP for a resource that publishes nothing, and the one PDP that is called with shared_secret. Its origin is always permitted, even over http. Must be an http or https URL with a host, or a vault reference. |
shared_secret required · ref · enc | string | The Sideband API shared secret PingAuthorize expects from this PEP. Sent to service_url only; a discovered PDP is called with what pdp_credentials names for it, or with nothing. |
secret_header_name required | string | The header the secret travels in, as configured on the PingAuthorize Sideband API shared secret (commonly CLIENT-TOKEN). Also the default header for pdp_credentials entries. |
connection_timeout_ms | integer · 10000 | Timeout on each sideband call. |
discovery_timeout_ms | integer · 5000 | Timeout on each metadata document or resolver answer discovery fetches. |
connection_keepAlive_ms | integer · 60000 | Keep-alive on the connection pool to each PDP. |
verify_service_certificate | boolean · true | TLS verification on every sideband, metadata and resolver call. A PEP that accepts any certificate has no integrity on the decision it enforces. Off needs allow_insecure; trust PingAuthorize's self-signed certificate with lua_ssl_trusted_certificate instead. |
enable_debug_logging | boolean · false | Log each sideband call and its outcome at debug level. |
The route
| Field | Type · default | Meaning |
|---|---|---|
pep_label | string · kong-sideband-pep | Names this PEP in the X-PDP-PEP response header. |
resource | string | The protected resource's identifier (RFC 8707), the key discovery starts from. A route without one uses service_url. |
forward_client_certificate | boolean · true | Send the client's TLS certificate as client_certificate: its public key as a JWK, the certificate in x5c - what PingAuthorize policy reads for a certificate-bound client. A certificate that cannot be parsed is a 400, as in ping-auth. |
filter_response | boolean · true | Make the /sideband/response calls, so policy can filter and rewrite the upstream's response. Off, the response phase only marks the response. Kong buffers the whole upstream response on any route this plugin is on, on or off - see compatibility. |
max_request_body_size | integer · 1048576 | The largest request body, in bytes, sent to the policy provider. A body Kong buffered to disk is read back up to this (Kong 3.9+); a larger one is a 413, never sent without its body. |
max_response_body_size | integer · 1048576 | The largest upstream response body, in bytes, sent to the policy provider. A larger one is withheld with a 502 rather than filtered in part or passed unfiltered. |
policy_path | client | resource · client | The path the policy provider is sent. client is the path the client asked for, route prefix included, as Ping's plugin sends it. resource is the path the upstream receives (after strip_path, under the service's path) less the path of resource, which is how the resource's RFC 9728 metadata names its operations; a policy's rewrite of the path is put back under the resource's path. |
PDP discovery
| Field | Type · default | Meaning |
|---|---|---|
pdp_discovery | off | resource | federation-resolver · off | off: service_url, no fetch. resource: read the route's resource's RFC 9728 document for authzen_policy_set (the PDPs that decide for it, and how their answers combine), falling back to service_url when the resource publishes nothing. federation-resolver: read the same parameter from the resource's resolved oauth_resource metadata, from a federation resolve endpoint, and never from the resource's own document. No PDP metadata is probed in any mode: a sideband endpoint has none, and an identifier is the base its endpoints hang off. |
pdp_metadata_ttl | number · 300 | Cache TTL, seconds, for resource and resolved metadata. A fetch that fails, or an answer that does not validate, serves the last good answer for up to one more TTL, and never past a resolved answer's own exp; after that, or with none, the layer is unavailable and its rule decides. Must be greater than zero. |
pdp_allowlist | array of string | Prefixes a discovered or configured PDP must match; service_url is always permitted. Every PDP a published policy set names passes this too. Required with discovery on. |
resource_metadata_allowlist | array of string | Prefixes a route's resource must match before its metadata is looked up, in either mode. Empty means any. An entry may carry a path. |
honour_published_fail_open | boolean · false | Act on a fail_open a member carries in the resource's own RFC 9728 document. Self-asserted, so ignored by default. Under federation-resolver a published fail_open is honoured unless the resolved metadata carries authzen_fail_open_permitted: false. |
pdp_discovery_insecure | boolean · false | Allow http for discovered URLs and the resolve endpoint. Dev only, and needs allow_insecure; service_url's own origin is always trusted over http. |
The resolver switch
| Field | Type · default | Meaning |
|---|---|---|
federation_resolve_url | string | A federation resolve endpoint (OpenID Federation 1.0 §8.3), usually the trust anchor's. Required in federation-resolver mode, and https unless allow_insecure. Called as GET {url}?sub={resource}&anchor={federation_trust_anchor} with Accept: application/resolve-response+jwt. Configuration, not something discovered, so no allowlist bounds it; the URL policy still does. |
federation_trust_anchor | string | The trust anchor's entity identifier the resolver resolves under. Required in federation-resolver mode. |
The plugin trusts the configured resolver through TLS. It decodes the resolve-response JWT without verifying its signature. It checks typ, sub, expiry and the listed PDP identifiers. The mode and source fields use federation-resolver to identify this behaviour. Use coaz-pep's federation mode when the enforcement point must verify the full trust chain against configured anchor keys.
| The resolver answers | Discovery does |
|---|---|
not_found, or a 404 | No metadata: service_url decides, as for a resource outside the federation. |
invalid_trust_chain, invalid_metadata, invalid_subject, invalid_trust_anchor, invalid_request, unsupported_parameter, any other 4xx | A refusal: the request fails with a 503 whatever the failure rule says, and a cached answer is dropped, so revocation at the anchor reaches a running gateway. A resource that claims membership and fails validation is a signal, not an outage. |
server_error, temporarily_unavailable, a 5xx, unreachable | Transient: the cached answer while there is one and it has not passed its exp, then the layer's rule. |
a response that is not a JWS, has the wrong typ, has expired, or lists something that is not a PDP identifier | An invalid document: logged, not used; service_url decides. |
Layers and their rules
| Field | Type · default | Meaning |
|---|---|---|
pdp_layers | array of string · ["resource"] | The ordered PDPs to ask, every one of which must permit; the first that does not is the answer. Each entry is <static | resource | PDP identifier> [fail-open | fail-closed] [request-only]. static is service_url regardless of discovery; resource is the resource's policy set (authzen_policy_set, or the deprecated pair read as one), which the entries wrap, deny-overrides; an identifier is a PDP. The resource entry's rule is the default every member of the published set inherits, and every PDP the set names passes pdp_allowlist; a configured entry naming the same PDP is one call, each applying its own rule. request-only skips the layer's /sideband/response call. An entry the plugin cannot read fails the route closed. |
fail_mode | closed | open · closed | What a layer does when its PDP is unavailable - no answer, a 5xx, a 429 - unless the entry says for itself. closed denies with a 503 temporarily_unavailable, carrying the PDP's Retry-After when it answered a 429 (that used to be a 429); the next request is asked afresh. open skips the layer; if every layer was skipped the request is permitted, and any permit that skipped one carries X-PDP-Fail-Open. A deny is a decision, never skipped; a refusal - a 3xx or 4xx (401 for a missing secret included), an answer it cannot read or apply, a payload it could not encode, the allowlist, an invalid chain - never opens. |
Credentials
| Field | Type · default | Meaning |
|---|---|---|
pdp_credentials | array of record | What a discovered or configured PDP is called with, by identifier: pdp (required), shared_secret (required · ref · enc), secret_header_name (defaults to the top-level one). shared_secret is bound to service_url and goes nowhere else. A PDP with no entry is called with nothing; PingAuthorize then answers 401, which is a refusal - closed whatever the layer's rule - so a resource that names a PDP this gateway holds no secret for is a resource this gateway cannot serve, and says so. |
Safety and compatibility
| Field | Type · default | Meaning |
|---|---|---|
allow_insecure | boolean · false | The one way past the configuration refusals below: discovery without pdp_allowlist, pdp_discovery_insecure, verify_service_certificate: false, a plain-http resolver. For development and demos. Kong logs what it relaxes, per route, when the configuration loads. |
protocols | http, https, grpc, grpcs | Kong's own field: which protocols the plugin runs on. |
Refused at configuration time, each with a message saying which: pdp_discovery: federation-resolver without federation_resolve_url or without federation_trust_anchor; a service_url that is neither an http or https URL with a host nor a vault reference; and, unless allow_insecure, discovery without pdp_allowlist, pdp_discovery_insecure: true, verify_service_certificate: false, and an http federation_resolve_url. The old mode name, federation, is refused.
REST routes only. A plugin with a response phase makes Kong buffer the whole upstream response on every route it is on, filter_response on or off. That is incompatible with SSE, so an MCP route belongs behind authzen-pdp and coaz-pep. Kong also refuses to load a plugin that has both a response phase and a header filter, which is why the X-PDP-* headers are set in access and on the plugin's exits rather than in a later phase. Accept-Encoding is dropped from the proxied request so the body the response phase reads is one the policy provider can read.
Responses Kong would not show it. Kong runs the response phase only under buffered proxying, which it turns off for a websocket upgrade on every release and for HTTP/2 before 3.9. Where any layer filters responses, such a request is refused with a 400 before any PDP is asked; a route that filters nothing lets it through, marked. On Kong 3.9 and 3.15 an HTTP/2 request on a filtering route is served and filtered.
Plugin order. Priority 999, after Kong's authentication plugins and before ip-restriction (990), acl (950) and rate-limiting (910). See the Kong page for what that costs and the Enterprise ordering that changes it.
What PingAuthorize is sent
- The URL Kong routed on: its scheme, the
Host, its own port and the normalised path its router matched, escaped for the wire, with the query normalised as Ping did. NeverX-Forwarded-*, which Kong honours from any client intrusted_ips- one there could haveDELETE /admin/users/1judged as/public/status. The path is the client's, or withpolicy_path: resourcethe upstream's less the resource's. - The body, whole, up to
max_request_body_size, or a 413request_too_large. - Every header and query argument, up to 1,000 of each, or a 413
request_too_largefor headers and a 400invalid_requestfor arguments. - No client
X-Auth-*: those are removed from the payload and from the request to the upstream; a layer's rewrite may add them. - Two headers the plugin asserts:
X-PEP-Method, andX-AuthZEN-Resourcewhen the route names aresource. A client's copies are dropped from the payload and from the request to the upstream; a layer's answer cannot change them for the next layer, and they never reach the upstream.
A permit must have the permit's shape - method and url strings, headers a list of single-pair objects, body a string when present. Anything that is neither that nor a relayable response, a 2xx {} included, is a refusal.
Complete configurations
A REST API with discovery, a published layer and per-PDP credentials
_format_version: "3.0"
services:
- name: bank-api
url: http://bank-api:8070
routes:
- name: bank
paths: ["/bank"]
strip_path: true
plugins:
- name: sideband-pdp
config:
service_url: "{vault://env/paz-url}" # the static PDP; the one PDP called with shared_secret
shared_secret: "{vault://env/paz-secret}"
secret_header_name: CLIENT-TOKEN
pep_label: "PEP#2 (Bank API edge)"
pdp_discovery: resource
resource: https://api.bank.example
pdp_allowlist: ["https://pdp.bank.example", "https://pdp.estate.example"]
resource_metadata_allowlist: ["https://api.bank.example"]
pdp_credentials:
- pdp: https://pdp.bank.example/tenants/bank
shared_secret: "{vault://env/bank-pdp-secret}"
- pdp: https://pdp.estate.example
shared_secret: "{vault://env/estate-pdp-secret}"
secret_header_name: X-Estate-Token
pdp_layers: ["resource"] # what the document publishes, then its own PDP
fail_mode: closed
A route using a federation resolver
- name: sideband-pdp
config:
service_url: "{vault://env/paz-url}"
shared_secret: "{vault://env/paz-secret}"
secret_header_name: CLIENT-TOKEN
pdp_discovery: federation-resolver
resource: https://member.bank-federation.example
federation_resolve_url: https://anchor.bank-federation.example/resolve
federation_trust_anchor: https://anchor.bank-federation.example
pdp_allowlist: ["https://pdp.bank-a.example", "https://pdp.estate.example"]
pdp_credentials:
- pdp: https://pdp.estate.example
shared_secret: "{vault://env/estate-pdp-secret}"
An estate gate the route can live without
pdp_layers: ["static fail-open request-only", "resource"] # service_url first, as a gate; then the resource's stack
fail_mode: closed
What it puts on the wire
| Where | What | Meaning |
|---|---|---|
| to each PDP, access phase | POST {pdp}/sideband/request | The request Kong will forward: source_ip, source_port, method, url, http_version, headers (one pair per value), body, client_certificate; the PDP's secret in its header. Each layer sees the request as the previous one rewrote it. Back comes the request to forward, with a state, or under response the HTTP to send instead. |
| proxied request, on permit | the rewrites | Headers added, changed and removed; a new method, path, query or body - as the last layer left them. Accept-Encoding dropped; a client's X-Auth-* removed. |
| to each permitting PDP, response phase | POST {pdp}/sideband/response | The upstream's response_code, response_status, headers and body, with that layer's state (or the request it saw, when it returned none), in reverse order, skipping request-only layers. Back comes the response to send; a header the policy provider left out is removed. |
| response | X-PDP-PEP, X-PDP-Decision, X-PDP-Layers, X-PDP-Layer, X-PDP-Source, X-PDP-Fail-Open | Which PEP; PERMIT or DENY; the PDPs that permitted, in order; on a deny, whose answer it was; where the resource's PDP came from (static, rfc9728, federation-resolver); the identifiers of the layers skipped. On every permit, whether or not the response phase runs. Printable ASCII only. |
| response, on a policy deny | the policy's HTTP | Status, headers and body as PingAuthorize returned them - the WWW-Authenticate a stepped-up agent needs, and the refusal contract's body if the client is to see what authzen-pdp sends, are the policy's to write - with each header value made header-safe. A 413 from the sideband API is relayed the same way. |
| response, on the plugin's own deny | 503, 400, 413 | By the refusal contract (authzen-pdp/refusal.lua), as authzen-pdp and every other PEP here give it. 503 temporarily_unavailable: a PDP that could not be resolved, was unavailable under a closed rule, or refused, with the PDP's Retry-After when it was rate-limiting this gateway. 400 invalid_request: a client certificate that could not be parsed, a header the plugin cannot pass on, more than 1,000 query arguments, or an Upgrade or pre-3.9 HTTP/2 request on a filtering route. 413 request_too_large: a body or a header set too large to send whole. Body {"status","error","error_description"} with a generic description, and no WWW-Authenticate. It used to be {"error":"authorization_failed",…}, with a 429, 414 or 431 for some of these. |
| response, after the upstream ran | 502 | A response-phase layer that could not be reached, whatever its rule (a 429 included), or that refused, or a response larger than max_response_body_size: every upstream header is cleared and the answer is a 502 response_withheld, its description saying the upstream processed the request and its response could not be authorised. |
Component responsibilities
- PingAuthorize policy supplies the denial response. Configure it to return the required status and headers, including any authorisation challenge.
- PingAuthorize receives the token in the
authorizationheader, where policy can validate it. This plugin provides no JOSE verification. Useauthzen-pdpwith coaz-pep for MCP request authorisation and DPoP verification. - Use coaz-pep to verify
signed_metadatain RFC 9728 documents. - Use
authzen-pdpwithfederation_entity_urlon the metadata routes to relay the resource's well-known documents from coaz-pep.
The plugin is tested with busted against a mocked Kong (gateways/kong/spec/sideband-pdp_spec.lua, with the discovery rules in discovery_spec.lua) under the same coverage floor as authzen-pdp, and was run through Kong 3.7, 3.9 and 3.15 DB-less with both plugin directories mounted, against a stub that plays the sideband endpoints and an echoing upstream: the forwarded-path, HTTP/2, Upgrade and withheld-response behaviour above is what those runs showed.
Upgrading from 0.1
pdp_discovery: federationis nowfederation-resolver, andX-PDP-Sourcesays so; the old name is refused.- Discovery needs
pdp_allowlist;pdp_discovery_insecure,verify_service_certificate: falseand an http resolver needallow_insecure. - A 401 or 403 from PingAuthorize is a refusal, closed even on a fail-open layer; a 429 no longer blocks the PDP for the worker.
- The URL sent is the one Kong routed on, not
X-Forwarded-*. - Oversized bodies, too many headers or query arguments, and
Upgrade(or, before Kong 3.9, HTTP/2) on a filtering route are refused rather than judged in part or not filtered at all. - A response-phase failure is a 502 with the upstream's headers withheld.
- The rock is
kong-plugin-sideband-pdp-0.4.2-1, depending onkong-plugin-authzen-pdp0.4.2.