Files
homelab/k8s/apps/api/AUTH-PLAN.md
T

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 (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 isshttps://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)

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.