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

The Kong sideband plugin

sideband-pdp is a Kong plugin that enforces through PingAuthorize's Sideband API — the request Kong will forward to /sideband/request in the access phase, the upstream's response to /sideband/response in the response phase — and finds the PDPs it calls from the resource it protects: the resource's RFC 9728 metadata, or, with the switch, a federation resolve endpoint. It is the Kong plugin's discovery, layers and failure rules from the modules the two share, in front of a PDP protocol where the policy owns the request mapping and the shape of a deny. Every knob is plugin configuration on a route or service. Version 0.4.0; Kong Gateway 3.4 or later, 3.9 or later 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.0-1.all.rock on the release, depends on kong-plugin-authzen-pdp 0.4.0 for them: install that one first.

# 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

Grouped as they are used, not as the schema lists them. Types are the schema's; 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.
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 its own denials and 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.

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_decision_points (the PDP that decides for it; the first this route may call wins) and authzen_policy_layers (the PDPs to ask in front of it), falling back to service_url when the resource publishes nothing. federation-resolver: read the same two parameters 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. A published layer 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.
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.

What the switch is, and is not: the resolver's word, taken on transport. The resolve response is a JWT this plugin cannot verify — no JOSE verifier is available to a Kong plugin — so it is decoded, not verified. The plugin checks what it can: the response's typ is resolve-response+jwt, its sub is the resource asked about, it has not expired, and what it lists are PDP identifiers. The trust is TLS to a URL the operator configured, exactly the trust placed in service_url. That is a weaker claim than coaz-pep's federation mode, which walks the chain and verifies every signature to an anchor key it holds — which is why the mode, X-PDP-Source and context.resource_metadata_source all say federation-resolver, never federation. A route that needs the stronger claim belongs behind coaz-pep.

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 what discovery finds, behind whatever the document publishes in authzen_policy_layers; an identifier is a PDP. A published layer takes the resource entry's rule and passes pdp_allowlist; a configured entry naming the same PDP is one call, and its own rule wins. 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 (a 429 with the PDP's Retry-After when that is what it said; 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 that takes the federation resolver's word

      - 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 is 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, 429, 400, 413, 414, 431A PDP that could not be resolved, was unavailable under a closed rule, or refused (503); a PDP rate-limiting this gateway (429 with Retry-After); a client certificate that could not be parsed, or an Upgrade or pre-3.9 HTTP/2 request on a filtering route (400); a body, headers or query too large to send whole (413, 431, 414). Body {"error":"authorization_failed","pep":…,"reason":…}, the shape every PEP here uses, with a generic reason.
response, after the upstream ran502A response-phase layer unavailable under a closed rule, or refusing, or a response larger than max_response_body_size: every upstream header is cleared and the body says the upstream processed the request and its response was withheld.

What it does not do

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