3.9 KiB
3.9 KiB
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 <jwt> ──▶ 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(typeservice_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 staticinvokescope) — no user scopes needed for M2M.
- Client secret written to k8s Secret
api/model-invoke-oidc(keysclient-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). jwtcredential on that consumer (a Secret of typekonghq.com/v1/credential):algorithm: RS256key= the tokeniss→https://authentik.riotpiao.com/application/o/model-invoke/rsa_public_key= the PEM public key of Authentik'smodel-invokesigning 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-authto each model Ingress'skonghq.com/pluginsannotation (currently e.g.llm-rewrite-reasoning) → becomesllm-rewrite-reasoning,jwt-auth. - Leave
/modelslist route open OR protect too (decision).
Client usage (after build)
TOKEN=$(curl -s https://authentik.riotpiao.com/application/o/token/ \
-d grant_type=client_credentials \
-d client_id=model-invoke \
-d client_secret=<secret> \
-d scope=openid | jq -r .access_token)
curl https://api.riotpiao.com/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" -d '{...}'
Test plan
- No token → Kong returns 401.
- Valid client_credentials token → 200, model responds.
- Expired/garbage token → 401.
- Confirm the
/modelsroute behaviour matches the decision.
Open items / risks
- Authentik
client_credentialsfor 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
jwtmaps token→consumer by theiss=keymatch; ensure the provider's issuer is stable.