Configuring Kong

Prev Next

This page walks you through installing the PlainID plugins on Kong Gateway and configuring each one on a route.
For an introduction to the plugins and what each one does, see Kong Gateway.

You install the plugins once per data plane. After that, the only thing you manage in Kong is each plugin's configuration on the routes you protect. PlainID evaluates every request against your Policies at request time, so you change who can do what in PlainID, not in Kong.

Prerequisites

Before you start, make sure you have:

  • Kong Gateway 3.15 or later on a self-managed data plane. If your data plane connects to Konnect, run the same gateway version as your control plane. A data plane older than its control plane can fail to sync configuration.
  • Your PlainID PDP URL and a Client ID from the PlainID Scope for this integration. If the Scope authenticates with a client secret, you also need the Client Secret.
  • For plainid-prompt-authz: Kong AI Gateway (ai-proxy or ai-proxy-advanced) on the LLM route, the URL of your PlainID AI Content Controls Service, and the names of your Prompt Control and Output Control Asset Types.
  • For plainid-insights, or for MCP discovery with plainid-mcp-authz: your PlainID Collector URL and a client certificate and key for mutual TLS.

Installing the Plugins

The plugins ship as five LuaRocks packages: one per plugin, plus a shared PlainID library that every plugin depends on.

To install the plugins:

  1. Install the shared library first. Each plugin package lists it as a dependency, and LuaRocks fails if it tries to fetch the library from the public registry.

    luarocks make plainid-authz-common-0.2.1-1.rockspec
    
  2. Install the plugins you require. You don't need to install all four.

    luarocks make kong-plugin-plainid-api-authz-0.1.1-1.rockspec
    luarocks make kong-plugin-plainid-mcp-authz-0.1.1-1.rockspec
    luarocks make kong-plugin-plainid-prompt-authz-0.3.1-1.rockspec
    luarocks make kong-plugin-plainid-insights-0.1.0-1.rockspec
    
  3. Enable the plugins in Kong, either through the KONG_PLUGINS environment variable or the plugins field in kong.conf:

    KONG_PLUGINS=bundled,plainid-api-authz,plainid-mcp-authz,plainid-prompt-authz,plainid-insights
    
  4. Restart Kong.

Use the PlainID Data Plane Image

PlainID also provides a ready-made data plane image based on kong/kong-gateway:3.15. The image contains all four plugins and installs them from the same packages, so it always matches a LuaRocks installation. Pin the image tag to your Konnect control plane's gateway version.

Register the Plugins on Konnect

If you manage Kong through Konnect, register each plugin's schema once at the control plane level:

  1. Open Gateway Manager and select your control plane.
  2. Go to Plugins > Custom Plugins.
  3. Select + New Custom Plugin.
  4. Upload the plugin's schema.lua.
  5. Repeat for each plugin you installed on your data planes.

See Kong Konnect's documentation for more information.

After you install or register the plugins, attach each one to its Services or Routes through the Kong Admin API, declarative configuration, or Konnect.

Connect to PlainID

The three enforcement plugins (plainid-api-authz, plainid-mcp-authz, and plainid-prompt-authz) share the same settings for connecting to the PlainID PDP.

Field Default Required Description
plainid_pdp_url Yes The PDP base URL, up to and including /runtime. For example, https://<pdp-host>/api/runtime.
client_id Yes The Client ID from your PlainID Scope.
authentication_method token No How the plugin authenticates to PlainID: token, secret-header, or secret-body. Must match your Scope's authentication method. See the table below.
client_secret With secret-header or secret-body The Client Secret from your PlainID Scope. Store it as a vault reference. See Protect the Client Secret.
user_auth_header Authorization No The inbound header that carries the caller's credential. The plugin forwards it to PlainID as a Bearer token.
token_forward_headers [] No Additional inbound headers to send to PlainID, for Scopes that identify callers by header.
runtime_request_timeout 60 No Seconds to wait for the PDP. On timeout, the plugin denies the request.

Set authentication_method to match the authentication method on your PlainID Scope. A mismatch always returns a 401 from PlainID, which the plugin reports as a denial.

Scope authentication method authentication_method
Authorization JWT, or None token
Client Secret in header secret-header
Client Secret in request secret-body

In every mode, the plugin forwards the caller's credential to PlainID so that PlainID can identify the caller. The secret modes send the Client Secret in addition to the caller's credential, not instead of it.

Match Your Kong Authentication Plugin

The plugins need to know who the caller is. How you set this up depends on the Kong authentication plugin on the route.

Kong authentication plugin Setup
None, or jwt Use any authentication_method. The caller's JWT already arrives on the Authorization header.
jwt-signer Set user_auth_header to the header you configured as jwt-signer's original_access_token_upstream_header. jwt-signer can replace Authorization with its own token.
key-auth, basic-auth, ldap-auth, hmac-auth, mtls-auth, oauth2, oauth2-introspection These plugins don't produce a JWT. Use secret-header or secret-body, set token_forward_headers: ["X-Consumer-Username"], and configure a header matcher on that value in your PlainID Scope.
openid-connect, authorization code flow No caller JWT reaches the plugin. Use a secret mode, map the claims you need to headers with openid-connect's upstream_headers_claims, and configure a matching header matcher in your PlainID Scope.
openid-connect, Bearer flow openid-connect replaces Authorization with its own token. Contact PlainID Support to validate this setup.

Protect the Client Secret

  • Konnect: Always store client_secret as a vault reference. Konnect returns plugin configuration in plain text to anyone who can list plugins, so anyone with that access can read a literal secret.
  • Self-managed Kong with declarative configuration: A literal secret works, but we recommend the env vault, which keeps the secret out of kong.yaml: {vault://env/PLAINID_CLIENT_SECRET}.
  • The encrypted setting on client_secret takes effect only with Kong Enterprise keyring encryption.

Trust the PDP Certificate

Kong verifies only one level of the certificate chain by default, which isn't enough for many public certificate chains. If the plugins can't connect to the PDP over HTTPS, set these variables on the Kong container:

KONG_LUA_SSL_TRUSTED_CERTIFICATE=system
KONG_LUA_SSL_VERIFY_DEPTH=3

Authorize REST API Calls

Attach plainid-api-authz to a REST API route. For each request, the plugin asks PlainID for a single permit or deny decision. Kong forwards permitted requests unchanged and returns an error for denied ones.

services:
  - name: cases-api
    url: https://<your-api-host>
    routes:
      - name: cases
        paths: [/cases]
        plugins:
          - name: plainid-api-authz
            config:
              plainid_pdp_url: https://<pdp-host>/api/runtime
              client_id: <your-client-id>
              authentication_method: token

plainid-api-authz needs no route-specific settings. It sends the full request to PlainID: the caller's IP, headers, path, query parameters, scheme, method, and body. If the body is JSON, PlainID receives it as a JSON object, so your Policies can use body attributes directly. The plugin always sends the body, because REST Policies often depend on it.

If a request body is larger than Kong's client_body_buffer_size, the plugin can't read it. The plugin still asks PlainID for a decision, but without body attributes, and writes an Error trace line. If your Policies depend on body attributes, raise client_body_buffer_size to cover your largest requests.

By default, a denied request returns 403 with the body { "code": 403, "error": "Forbidden" }. You can change the denial response with these fields:

Field Default Description
on_prevent_status_code 403 HTTP status code for denied requests.
on_prevent_body { "code": 403, "error": "Forbidden" } Response body for denied requests.
on_prevent_content_type application/json Content type of the denial response.
enable_tracing false Writes a trace line for each decision. See Troubleshoot With Trace Logs.

MCP Tools

Attach plainid-mcp-authz to an MCP server route. The plugin:

  • Filters each tools/list response, so a caller sees only the tools its Policies allow.
  • Authorizes each tools/call request, and blocks tool calls the caller may not make before they reach the MCP server.
  • Reports the MCP server's full tool inventory to the PlainID Collector, so PlainID can discover the tools and you can write Policies for them.

Use the mode field to choose what the plugin does:

mode Behavior
discovery Reports tool inventories only. Makes no PDP calls, so it keeps working even when the PDP is unavailable.
enforcement Filters tools and authorizes tool calls only.
both Discovery and enforcement.

This example puts three plugins on a route in front of an MCP server. The first validates the caller's token, plainid-mcp-authz decides what the caller can do, and the last swaps in the credential the upstream MCP server expects.

services:
  - name: snowflake-mcp
    url: https://<account>.snowflakecomputing.com/api/v2/.../mcp-servers/<server>
    routes:
      - name: snowflake
        paths: [/snowflake]
        plugins:

          # 1. Validate the caller's token.
          - name: ai-mcp-oauth2
            config:
              authorization_servers: [https://<your-idp>/v2.0]
              passthrough_credentials: true

          # 2. Ask PlainID which tools this caller may see and use.
          - name: plainid-mcp-authz
            config:
              mode: both
              plainid_pdp_url: https://<pdp-host>/api/runtime
              client_id: <your-client-id>
              authentication_method: token
              mcp_resource_type: Tools

          # 3. Replace the caller's token with the upstream's credential.
          - name: request-transformer
            config:
              replace:
                headers:
                  - Authorization:Bearer <snowflake-token>

With this configuration, an Agent that lists the server's tools sees only the tools its Policies allow. If the Agent calls a tool it isn't authorized to use, Kong blocks the call and the request never reaches the MCP server.

Configuration Reference

In addition to the PlainID connection settings:

Field Default Required Description
mode both No discovery, enforcement, or both.
mcp_resource_type Tools No The Asset Type for MCP tools in PlainID. Must match the Asset Type name in your Policies.
mcp_error_code -32603 No JSON-RPC error code for denied requests.
mcp_error_message Access Denied No JSON-RPC error message for denied requests.
enable_tracing false No Writes a trace line for each decision and each discovery step.

How the Plugin Handles MCP Requests

Request What the plugin does
tools/list Asks PlainID which tools the caller may use, and removes every other tool from the response. Works with JSON and streamed (SSE) responses.
tools/call Asks PlainID whether the caller may call this tool, and blocks the call if not.
Other MCP methods, such as initialize Forwards them without a PlainID call, and adds the header X-Authorized-By: PlainID (MCP bypass). These methods don't expose or run tools.
JSON-RPC batch requests Denied. A batch can contain any number of tool calls.
tools/call without a tool name Denied, without a PlainID call.
A request body the plugin can't read Denied. Without the body, the plugin can't tell a tool call from other MCP traffic.
A request that isn't JSON-RPC Forwarded, with a Skipped trace line. This usually means the route serves REST traffic and needs plainid-api-authz instead.

For tools/call, the plugin sends the tool's arguments to PlainID at the root of the request body. Your Policies can refer to an argument directly, for example $.branch.

If the MCP server returns a compressed tools/list response, the plugin can't filter it and denies the response. The plugin asks the server for uncompressed responses, so this happens only when a server ignores that request.

MCP Denial Responses

MCP clients match every response to a pending call by its JSON-RPC id. A plain HTTP error doesn't reach that layer, so the client would wait until its own timeout. Instead, the plugin returns HTTP 200 with a JSON-RPC error that echoes the request id:

{"jsonrpc":"2.0","id":7,"error":{"code":-32603,"message":"Access Denied"}}

You can change the error code and message with mcp_error_code and mcp_error_message. The rest of the response follows the JSON-RPC format and you can't change it.

Discovery Requirements

Discovery records the server's full tool inventory, before the plugin filters it for the caller, so every caller's tools/list reports the same inventory. Discovery never affects the caller's response: if it fails, the plugin logs the failure and the response continues normally. Discovery skips a tool inventory it can't attribute to a specific MCP server and gateway. Make sure that:

  • Every MCP route has a name. PlainID uses the route name to identify the MCP server. Kong allows routes without names, but discovery skips them and logs DiscoverySkippedUnnamedRoute.
  • Your data plane connects to a Konnect control plane. PlainID uses the control plane ID to identify the gateway. On a data plane without Konnect, discovery stops and logs DiscoverySkippedNoClusterId. Enforcement keeps working.

Neither case returns an error to callers, so turn on enable_tracing the first time you deploy and confirm that neither event appears.

Control Prompts and Responses

Attach plainid-prompt-authz to an LLM route served by Kong AI Gateway. The plugin works in two directions:

  • Prompt Control (input): Before the request reaches the LLM provider, the plugin checks which content categories the conversation touches and blocks the request if the caller isn't permitted to use them.
  • Output Control (output): Before the model's response reaches the caller, the plugin masks or encrypts sensitive data in it, based on the caller's Output Control Policy.

Use the mode field to choose which directions the plugin controls:

mode Behavior
prompt Prompt Control only.
output Output Control only.
both (default) Prompt Control and Output Control.

Where to Attach It

Attach plainid-prompt-authz to routes whose AI Gateway route_type is one of:

  • llm/v1/chat: OpenAI chat format, and Anthropic native chat format (llm_format: anthropic).
  • llm/v1/completions: OpenAI legacy completions.
  • llm/v1/responses: OpenAI Responses API.

The plugin applies only to routes you attach it to. Other AI Gateway route types, such as embeddings, audio, images, files, batches, assistants, and real-time WebSocket traffic, don't carry a text prompt the plugin can read. On those routes the plugin lets requests pass unchanged.

How Prompt Control Works

For each request, the plugin:

  1. Extracts the prompt and conversation from the request.
  2. Asks the PlainID PDP which Prompt Control categories the caller may use.
  3. Asks the PlainID AI Content Controls Service which categories the conversation touches.
  4. Compares the two sets and permits or denies the request.

The plugin never changes the prompt or request body it forwards. It does handle these requests specially:

  • Compressed requests: The plugin denies a request whose body uses a Content-Encoding other than identity, because it can't read the prompt.
  • Requests without a body: The plugin forwards them unchanged. They can't carry a prompt.
  • Empty conversations: In prompt and both modes, the plugin denies a request in a supported format that contains no prompt text. For example, an empty messages array, or a Responses API request that refers to a stored prompt or an earlier response instead of sending its own input. In output mode, the plugin forwards these requests and still masks the response.

The plugin removes any X-Authorized-By header the caller sends, and adds X-Authorized-By: PlainID only after Prompt Control permits the request. In output mode, the plugin doesn't add the header.

How Output Control Works

The PDP decision the plugin already fetched for the request includes the caller's Output Control Policy, so Output Control needs no second PDP call. The plugin holds the model's response, sends each text field to the AI Content Controls Service for masking, and returns the masked response to the caller. The rest of the response, including IDs, the model name, and token usage, stays the same. The masked response keeps the upstream status code, Content-Type, and JSON structure, so existing clients and SDKs parse it normally.

For each entity in the caller's Output Control Policy:

Policy action Result in the response
MASK The plugin replaces the value with ***.
ENCRYPT The plugin replaces the value with an encrypted value.
Both MASK and ENCRYPT The plugin masks the value.

ENCRYPT requires an encryption key on your AI Content Controls Service deployment, set through PLAINID_ANONYMIZATION__ENCRYPT_KEY. Without it, the service rejects the request, and the plugin denies the response instead of returning it unmasked.

The plugin masks the model-generated text in these fields:

Response format Masked fields
OpenAI chat Each choice's message.content
OpenAI completions Each choice's text
OpenAI Responses Each output_text item's text
Anthropic messages Each text block, and each thinking block

Output Control has these limits:

  • It applies to responses only. The plugin doesn't mask the prompt, so data a user types reaches the LLM provider as written. It also doesn't mask conversation history or tool results inside a request. Because the plugin masks every response, a client that sends its history back already sends masked text.
  • It doesn't mask tool call arguments. Changing them would break the tool call the client is about to make.
  • It doesn't support streaming. In output and both modes, the plugin denies any request with stream: true, and any request with an Upgrade header.
  • It supports specific LLM providers. When you use ai-proxy, Output Control supports OpenAI, Azure OpenAI, Anthropic, and Amazon Bedrock (Converse API). In output and both modes, the plugin denies successful responses from other providers, such as Gemini, Cohere, Llama, Mistral, and Hugging Face. Bedrock support requires Kong Gateway 3.6 or later.

Example

services:
  - name: openai-chat
    routes:
      - name: chat
        paths: [/chat]
        plugins:
          - name: ai-proxy
            config:
              route_type: llm/v1/chat
              # ... your provider and model settings

          - name: plainid-prompt-authz
            config:
              mode: both
              plainid_pdp_url: https://<pdp-host>/api/runtime
              client_id: <your-client-id>
              authentication_method: token
              ccs_url: https://<content-controls-host>
              prompt_control_asset_type: <your-prompt-control-asset-type>
              output_control_asset_type: <your-output-control-asset-type>

Configuration Reference

In addition to the PlainID connection settings:

Field Default Required Description
mode both No prompt, output, or both.
ccs_url Yes Base URL of your PlainID AI Content Controls Service.
ccs_request_timeout 30 No Seconds to wait for the AI Content Controls Service. On timeout, the plugin denies the request.
prompt_control_asset_type In prompt and both modes Your Prompt Control Asset Type. Matches exactly and is case-sensitive.
output_control_asset_type In output and both modes Your Output Control Asset Type. Matches exactly and is case-sensitive.
category_match all No all: PlainID must permit every category the conversation touches. any: one permitted category is enough.
include_system_prompt true No Whether to include system messages when checking the conversation.
on_no_category permit No permit or deny, when the conversation touches no categories at all.
upstream_response_format auto No The response format to mask. auto detects OpenAI and Anthropic responses, and Bedrock responses when ai-proxy uses the Bedrock provider. openai or anthropic accepts only that format and denies any other. bedrock always treats the response as a Bedrock Converse response, for routes that call Bedrock without ai-proxy.
on_prevent_status_code 403 No HTTP status code for denied requests.
on_prevent_body { "code": 403, "error": "Forbidden" } No Response body for denied requests.
on_prevent_content_type application/json No Content type of the denial response.
enable_tracing false No Writes a trace line for each decision.

Prompt Control uses category_match, include_system_prompt, and on_no_category only in prompt and both modes.

Upgrading from an earlier version: The default mode is now both, which requires output_control_asset_type. If your existing configuration has no output_control_asset_type, Kong rejects it. Either add output_control_asset_type, or set mode: prompt to keep Prompt Control only.

Deployment Considerations

  • Allow time for long responses. In output and both modes, the plugin holds the complete response before masking it. Set Kong's proxy timeouts, such as upstream_read_timeout, high enough for your longest generations.
  • Masked responses skip some ai-proxy features. When the plugin masks a response, Kong skips ai-proxy's token usage analytics and the X-Kong-LLM-Model response header for that response. The response body keeps the same format either way.
  • Other plugins that buffer responses. plainid-prompt-authz controls Kong response buffering on its route. A custom plugin on the same route that depends on response buffering can lose it. ai-proxy isn't affected.

Send Transactional Data With Insights

Attach plainid-insights to any route to send transactional data for each inbound request to the PlainID Collector. PlainID uses this data for agentic insights. You can attach it on its own, or alongside plainid-api-authz, plainid-mcp-authz, or plainid-prompt-authz. Insights never blocks or changes a request, and it sends data for requests that other plugins deny or rate-limit.

plugins:
  - name: plainid-insights
    config:
      collector_url: https://<collector-host>/collect
      collector_certificate_id: <kong-certificate-id>
      enable_tracing: true
Field Default Required Description
collector_url Yes Full Collector endpoint. Must use https.
collector_certificate_id One of these two The ID of a Kong certificate entity that holds the client certificate and key.
collector_client_cert One of these two The client certificate PEM, inline or as a vault reference.
collector_client_key With collector_client_cert The client key PEM, inline or as a vault reference.
collector_timeout_ms 1000 No Milliseconds to wait for the Collector. The request never waits for the Collector.
enable_tracing true No Writes a trace line when sending to the Collector fails. Keep this on: without tracing, a Collector failure is silent.

Provide exactly one certificate source: either collector_certificate_id, or collector_client_cert together with collector_client_key. If you store collector_client_key as a vault reference, Kong doesn't check that it matches the certificate, so a wrong reference only fails when the plugin sends data.

Insights Requirements

Like MCP discovery, Insights sends a request's transactional data only when PlainID can attribute it to a specific route and gateway. Make sure that:

  • Every route with Insights has a name. Insights skips requests on unnamed routes and logs InsightsSkippedUnnamedRoute.
  • Your data plane connects to a Konnect control plane. On a data plane without Konnect, Insights sends no transactional data and logs InsightsSkippedNoClusterId.

Neither case affects the request, so keep enable_tracing on and confirm that neither event appears after you deploy.

If a request body is too large for Kong to read into memory, Insights still sends the request's transactional data, without the body, and logs InsightsBodyUnreadable. If anything else goes wrong while Insights captures or sends the data, it drops that request's data and logs the failure. Insights never delays, blocks, or fails a request.

What Insights Sends

Insights sends the complete request as transactional data, so attach it only to routes whose traffic you're willing to store in the Collector:

  • The full request body, without redaction. On a login route, this includes the password.
  • Every request header, including Authorization, Cookie, X-Api-Key, and X-Client-Secret.
  • The full URL, including the query string.

You can't limit what Insights captures. The only control is which routes you attach it to. Treat the Collector as a store that holds credentials. plainid-mcp-authz discovery also sends request headers and the URL, but sends the tool inventory instead of the request body.

Insights uses one background timer per request while it sends to the Collector. By default, OpenResty allows 256 running timers. On a route above about 256 requests per second with a slow or unreachable Collector, Insights drops transactional data. If you attach Insights to a high-traffic route, raise lua_max_running_timers and keep enable_tracing on.

Insights also reads every request body into memory, including on routes that previously streamed request bodies to the upstream.

Failure Behavior

A caller receives the same denial response for a Policy denial and for an infrastructure failure. Use trace logs to tell them apart.

Condition Result
PlainID permits the request Kong forwards it unchanged and adds X-Authorized-By: PlainID.
PlainID denies the request Denied.
The PDP is unreachable, times out, or returns an error Denied.
The AI Content Controls Service is unreachable, times out, or returns an error Denied.
The request body is too large to read plainid-mcp-authz and plainid-prompt-authz deny the request. plainid-api-authz asks PlainID for a decision without body attributes. plainid-insights sends the transactional data without the body.
An LLM request body is compressed Denied.
An LLM request body contains no prompt the plugin recognizes Forwarded unchanged. Without a valid prompt, the LLM can't generate a response.
The upstream LLM returns a status other than 200 Forwarded unchanged.
The caller's Output Control Policy asks for no masking Response forwarded unchanged.
The LLM response is compressed, isn't JSON, or isn't in a supported format Denied.
Masking fails Denied. The plugin never returns a partly masked response.

Troubleshoot With Trace Logs

Set enable_tracing: true on a plugin to write one JSON line to the Kong log for each decision, prefixed with [plainid]:

[plainid] {"requestId":"<request id>","result":"Denied","details":"...","timestamp":"..."}
result Meaning
Success The plugin permitted the request, or completed a masking step.
Denied A Policy denied the request.
Error The plugin couldn't reach or read a response from PlainID, or couldn't read the request.
Skipped The plugin let the request pass because it wasn't in a format the plugin handles.

The requestId matches the X-Request-ID header, and the plugin sends the same ID to PlainID, so you can follow one request end to end. For plainid-prompt-authz, masking steps also include an event field, such as ResponseMasked or ResponseDenied, and a format field showing which response format the plugin masked.

Trace lines never contain credentials, at any tracing level.

Plugin Order

Kong runs plugins by priority, from highest to lowest. The PlainID plugins run after Kong's openid-connect and ai-mcp-oauth2 plugins, so the caller's identity is already resolved, and before Kong AI Gateway's ai-mcp-proxy and ai-proxy, so PlainID enforces Policies before AI Gateway transforms the request.

Plugin Priority
openid-connect (Kong) 1050
ai-mcp-oauth2 (Kong) 1015
plainid-insights 920
rate-limiting (Kong) 910
plainid-api-authz 909
plainid-mcp-authz 899
plainid-prompt-authz 890
ai-mcp-proxy (Kong) 820
ai-proxy (Kong) 770

© 2026 PlainID LTD. All rights reserved.