Kong
Kong verifies client certificates on its proxy listener against the projected bundle, and the mt-enforcer plugin asks the enforcer about every request before any other plugin runs.
Start the enforcer
Pair the enforcer (once)
mt-enforcer pair --api https://api.mutualtrust.io --state /var/lib/mt-enforcer
# shows a pairing code; approve it in the portal (Services > your service > Pair gateway)
mt-enforcer forward-auth (kong)
mt-enforcer forward-auth --format kong --state /var/lib/mt-enforcer \
--listen 127.0.0.1:9191 --bundle-out /etc/mt/client-ca.pemPlugin
services/enforcer/integrations/kong/plugins/mt-enforcer/handler.lua
-- mt-enforcer: Kong Gateway plugin that asks Mutual Trust's enforcer
-- (mt-enforcer forward-auth --format kong) about every request.
--
-- Kong terminates mTLS (see kong.conf / docker-compose.yaml for the proxy
-- listener's client-certificate settings). The plugin removes every inbound
-- x-mt-* header, sends the certificate of this connection and the original
-- request line to the enforcer in a request built from scratch, and either sets
-- the verified identity on the upstream request or returns the enforcer's denial.
-- If the enforcer cannot be reached the request is refused (fail closed).
local http = require "resty.http"
local MtEnforcer = {
PRIORITY = 1600, -- before authentication and transformation plugins
VERSION = "1.0.0",
}
local function header(res, name)
local v = res.headers[name]
if type(v) == "table" then return v[1] end
return v
end
-- Method and URL overrides would make the origin run a request other than the one authorized.
local overrides = {
["x-http-method-override"] = true, ["x-http-method"] = true, ["x-method-override"] = true,
["x-original-url"] = true, ["x-rewrite-url"] = true, ["x-original-method"] = true, ["x-original-uri"] = true,
}
function MtEnforcer:access(conf)
for name, _ in pairs(kong.request.get_headers(200)) do
local lower = string.gsub(string.lower(name), "_", "-")
if string.sub(lower, 1, 5) == "x-mt-" or lower == "x-client-cert" or overrides[lower] then
kong.service.request.clear_header(name)
end
end
local headers = {
["X-Original-Method"] = kong.request.get_method(),
["X-Original-URI"] = ngx.var.request_uri,
}
local cert = ngx.var.ssl_client_escaped_cert -- URL-encoded PEM leaf of this connection
if cert ~= nil and cert ~= "" then
headers["X-Client-Cert"] = cert
end
local traceparent = kong.request.get_header("traceparent")
if traceparent ~= nil then
headers["Traceparent"] = traceparent
end
local httpc = http.new()
httpc:set_timeout(conf.timeout)
local res, err = httpc:request_uri(conf.enforcer_url, {
method = "GET",
headers = headers,
keepalive_timeout = 60000,
keepalive_pool = 16,
})
if not res then
kong.log.err("mutual trust enforcer unreachable: ", err)
return kong.response.exit(503, { code = "enforcer_unavailable" })
end
local request_id = header(res, "X-Mt-Request-Id")
if res.status ~= 200 then
return kong.response.exit(res.status == 401 and 401 or 403, res.body, {
["Content-Type"] = "application/json",
["X-Mt-Request-Id"] = request_id,
["X-Mt-Reason"] = header(res, "X-Mt-Reason"),
})
end
local principal = header(res, "X-Mt-Principal")
if principal == nil or principal == "" then
return kong.response.exit(503, { code = "enforcer_response_invalid" })
end
kong.service.request.set_header("X-Mt-Principal", principal)
kong.service.request.set_header("X-Mt-Grant", header(res, "X-Mt-Grant"))
kong.service.request.set_header("X-Mt-Request-Id", request_id)
kong.response.set_header("X-Mt-Request-Id", request_id)
kong.response.set_header("X-Mt-Policy-Revision", header(res, "X-Mt-Policy-Revision"))
end
return MtEnforcer
services/enforcer/integrations/kong/plugins/mt-enforcer/schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "mt-enforcer",
fields = {
{ protocols = typedefs.protocols_http },
{ config = {
type = "record",
fields = {
{ enforcer_url = typedefs.url({ required = true, default = "http://127.0.0.1:9191/authz" }) },
{ timeout = { type = "integer", default = 1000, between = { 1, 10000 } } },
},
} },
},
}Declarative configuration
services/enforcer/integrations/kong/kong.yml
# Mutual Trust with Kong Gateway (DB-less declarative configuration).
# The mt-enforcer plugin runs on every route of the protected service.
_format_version: "3.0"
services:
- name: orders-api
url: http://origin:9001 # private network only
routes:
- name: orders-api
paths: ["/"]
protocols: ["https"] # the protected API is never served over plain HTTP
strip_path: false
preserve_host: true
plugins:
- name: mt-enforcer
config:
enforcer_url: http://127.0.0.1:9191/authz
timeout: 1000Run with Docker Compose
services/enforcer/integrations/kong/docker-compose.yaml
# Kong Gateway (DB-less) + mt-enforcer.
# docker compose run --rm enforcer pair --api https://api.mutualtrust.io --state /var/lib/mt-enforcer
# docker compose up -d
# The proxy listener requires a client certificate issued by the projected bundle
# (injected NGINX directives); the plugin then asks the enforcer per request.
services:
enforcer:
build: { context: ../.., dockerfile: Dockerfile }
image: mt-enforcer:1.0.0
command: ["forward-auth", "--format", "kong", "--state", "/var/lib/mt-enforcer", "--listen", "127.0.0.1:9191",
"--admin", "0.0.0.0:9464", "--bundle-out", "/var/run/mt/bundle/client-ca.pem"]
ports: ["8443:8443", "9464:9464"]
volumes: ["mt-state:/var/lib/mt-enforcer", "mt-bundle:/var/run/mt/bundle"]
healthcheck: { test: ["CMD", "/mt-enforcer", "healthcheck"], interval: 5s, retries: 30 }
kong:
image: kong:3.9.4@sha256:ad6cc0c8cca5b66b3cdab3bde7bc7ca973d4d820dd2c6651a7e3042f5428a76a
network_mode: "service:enforcer"
depends_on: { enforcer: { condition: service_healthy } }
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /kong/kong.yml
KONG_PLUGINS: bundled,mt-enforcer
KONG_LUA_PACKAGE_PATH: /kong/plugins-root/?.lua;;
KONG_PROXY_LISTEN: 0.0.0.0:8443 ssl http2
KONG_SSL_CERT: /kong/certs/server.pem
KONG_SSL_CERT_KEY: /kong/certs/server-key.pem
KONG_NGINX_PROXY_SSL_VERIFY_CLIENT: "on"
KONG_NGINX_PROXY_SSL_CLIENT_CERTIFICATE: /etc/mt/client-ca.pem
KONG_NGINX_PROXY_SSL_VERIFY_DEPTH: "2"
KONG_ADMIN_LISTEN: 127.0.0.1:8001
volumes:
- ./kong.yml:/kong/kong.yml:ro
- ./plugins/mt-enforcer:/kong/plugins-root/kong/plugins/mt-enforcer:ro
- ./certs:/kong/certs:ro
- mt-bundle:/etc/mt:ro
origin:
image: ${ORIGIN_IMAGE:-ghcr.io/your-org/orders-api:latest}
volumes: { mt-state: {}, mt-bundle: {} }KONG_NGINX_PROXY_SSL_VERIFY_CLIENT and KONG_NGINX_PROXY_SSL_CLIENT_CERTIFICATE inject ssl_verify_client and ssl_client_certificate into the proxy server block, so the handshake requires a certificate from the bundle and $ssl_client_escaped_cert is available to the plugin.
Contract
| Enforcer answer | Kong |
|---|---|
| 200 with x-mt-principal | Sets the identity headers upstream (inbound x-mt-* removed first) and continues. |
| 401 or 403 | Returns the enforcer status, JSON problem, x-mt-reason and x-mt-request-id. |
| unreachable or malformed | Returns 503 (fail closed). |
Tested