84 lines
3.9 KiB
Markdown
84 lines
3.9 KiB
Markdown
# 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` (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=<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.
|