# API Auth Layer — Authentik service account + Kong JWT (model invoke) Protect the model API (`api.riotpiao.com/*`, Kong OSS 3.9) so only an Authentik service account holding a valid **client_credentials** JWT can invoke the KServe models. "Invoke role" = **possession of a JWT from the dedicated model-invoke OAuth2 provider** (only the service account can obtain one). ## Flow ``` service account ── client_credentials ──▶ Authentik token endpoint (client_id + secret) https://authentik.riotpiao.com/application/o/token/ │ ▼ RS256 JWT (iss = https://authentik.riotpiao.com/application/o/model-invoke/) client ── Authorization: Bearer ──▶ Kong (api.riotpiao.com/*) jwt plugin: verify RS256 sig via Authentik JWKS, check iss/exp → map to KongConsumer → allow ▼ KServe model (reasoning / ornith / ...) ``` Kong OSS has no enterprise `openid-connect` plugin, so we use the built-in **`jwt`** plugin: it validates an RS256 signature against a public key we pin on a KongConsumer, keyed by the token's `iss`. ## Changes ### 1. Authentik (k8s/infra/iam/scripts/authentik-provision.py) - New **service account** user `model-invoker` (type `service_account`, no password; Authentik issues an app-password/token for M2M). - New **OAuth2 provider + application** `model-invoke`: - `client_type: confidential`, `grant_types: ["client_credentials"]` - signing key = existing RS256 keypair (same as other providers) - mappings: `openid` (+ optionally a static `invoke` scope) — no user scopes needed for M2M. - Client secret written to k8s Secret `api/model-invoke-oidc` (keys `client-id`, `client-secret`), labelled for whoever consumes it. - Bind the service account so it (and only it) can use the provider. ### 2. Kong (k8s/apps/api/, new file `model-auth.yaml`) - **KongConsumer** `model-invoker` (ns api). - **`jwt` credential** on that consumer (a Secret of type `konghq.com/v1/credential`): - `algorithm: RS256` - `key` = the token `iss` → `https://authentik.riotpiao.com/application/o/model-invoke/` - `rsa_public_key` = the PEM public key of Authentik's `model-invoke` signing cert (fetched from Authentik JWKS / cert, stored in git or ksops). - **KongPlugin** `jwt-auth` (`plugin: jwt`, `config.claims_to_verify: [exp]`). ### 3. Wire onto model routes (k8s/apps/api/llm-routes.yaml) - Add `jwt-auth` to each model Ingress's `konghq.com/plugins` annotation (currently e.g. `llm-rewrite-reasoning`) → becomes `llm-rewrite-reasoning,jwt-auth`. - Leave `/models` list route open OR protect too (decision). ## Client usage (after build) ```bash TOKEN=$(curl -s https://authentik.riotpiao.com/application/o/token/ \ -d grant_type=client_credentials \ -d client_id=model-invoke \ -d client_secret= \ -d scope=openid | jq -r .access_token) curl https://api.riotpiao.com/v1/chat/completions \ -H "Authorization: Bearer $TOKEN" -d '{...}' ``` ## Test plan 1. No token → Kong returns 401. 2. Valid client_credentials token → 200, model responds. 3. Expired/garbage token → 401. 4. Confirm the `/models` route behaviour matches the decision. ## Open items / risks - Authentik `client_credentials` for a *service account* may require an **app-password / JWT-assertion** flow rather than plain client_secret POST — verify Authentik 2026.x M2M exactly (client_credentials with client_secret vs the SA token). Adjust step 1 accordingly before wiring Kong. - Pinning `rsa_public_key`: Authentik key rotation would break it — document a rotation runbook, or have the provision script re-export the cert PEM into the Kong credential on each run (keeps them in sync, same idea as ksops secrets). - Kong `jwt` maps token→consumer by the `iss`=`key` match; ensure the provider's issuer is stable.