IDPartnersidp-auth-peps · reference
gateways/kong/sideband-pdp · Lua

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

FieldType · defaultMeaning
service_url required · refstringPingAuthorize'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 · encstringThe 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 requiredstringThe 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_msinteger · 10000Timeout on each sideband call.
discovery_timeout_msinteger · 5000Timeout on each metadata document or resolver answer discovery fetches.
connection_keepAlive_msinteger · 60000Keep-alive on the connection pool to each PDP.
verify_service_certificateboolean · trueTLS 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_loggingboolean · falseLog each sideband call and its outcome at debug level.

The route

FieldType · defaultMeaning
pep_labelstring · kong-sideband-pepNames this PEP in the X-PDP-PEP response header.
resourcestringThe protected resource's identifier (RFC 8707), the key discovery starts from. A route without one uses service_url.
forward_client_certificateboolean · trueSend 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_responseboolean · trueMake 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_sizeinteger · 1048576The 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_sizeinteger · 1048576The 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_pathclient | resource · clientThe 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

FieldType · defaultMeaning
pdp_discoveryoff | resource | federation-resolver · offoff: 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_ttlnumber · 300Cache 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_allowlistarray of stringPrefixes 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_allowlistarray of stringPrefixes 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_openboolean · falseAct 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_insecureboolean · falseAllow 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

FieldType · defaultMeaning
federation_resolve_urlstringA 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_anchorstringThe 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 answersDiscovery does
not_found, or a 404No 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 4xxA 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, unreachableTransient: 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 identifierAn invalid document: logged, not used; service_url decides.

Layers and their rules

FieldType · defaultMeaning
pdp_layersarray 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_modeclosed | open · closedWhat 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

FieldType · defaultMeaning
pdp_credentialsarray of recordWhat 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

FieldType · defaultMeaning
allow_insecureboolean · falseThe 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.
protocolshttp, https, grpc, grpcsKong'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

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

WhereWhatMeaning
to each PDP, access phasePOST {pdp}/sideband/requestThe 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 permitthe rewritesHeaders 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 phasePOST {pdp}/sideband/responseThe 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.
responseX-PDP-PEP, X-PDP-Decision, X-PDP-Layers, X-PDP-Layer, X-PDP-Source, X-PDP-Fail-OpenWhich 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 denythe policy's HTTPStatus, 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 deny503, 400, 413By 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 ran502A 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

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