From c4a302c3d1c06a51c90541d8c69ee90fef28d351 Mon Sep 17 00:00:00 2001 From: rock Date: Sun, 30 Aug 2026 20:13:41 -0700 Subject: [PATCH] feat(iam): add local-llm Authentik application with JWT auth - Add local-llm OAuth2 provider and application to Authentik provisioning - Configure JWT-compatible OAuth2 provider (client_id: local-llm) - Generate client secret on first run, stored in llm-serving/local-llm-jwt Secret - Bind llm-admins group to local-llm application for admin access - Add RBAC for provisioning job to create secrets in llm-serving namespace - Output JWT issuer URL and certificate for local-llm token validation --- k8s/infra/iam/authentik-provision-job.yaml | 14 +++++++++++ k8s/infra/iam/scripts/authentik-provision.py | 26 +++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/k8s/infra/iam/authentik-provision-job.yaml b/k8s/infra/iam/authentik-provision-job.yaml index 7bff3ac..7f3da6e 100644 --- a/k8s/infra/iam/authentik-provision-job.yaml +++ b/k8s/infra/iam/authentik-provision-job.yaml @@ -129,6 +129,20 @@ roleRef: name: authentik-provisioner apiGroup: rbac.authorization.k8s.io --- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: authentik-provisioner + namespace: llm-serving +subjects: + - kind: ServiceAccount + name: authentik-provisioner + namespace: iam +roleRef: + kind: ClusterRole + name: authentik-provisioner + apiGroup: rbac.authorization.k8s.io +--- apiVersion: batch/v1 kind: Job metadata: diff --git a/k8s/infra/iam/scripts/authentik-provision.py b/k8s/infra/iam/scripts/authentik-provision.py index c664c1c..c5d9bca 100644 --- a/k8s/infra/iam/scripts/authentik-provision.py +++ b/k8s/infra/iam/scripts/authentik-provision.py @@ -424,6 +424,16 @@ SERVICES = { "launch_url": "https://memory.riotpiao.com", "display_name": "Poimen Memory", }, + "local-llm": { + # JWT auth for local LLM API access - service-to-service, no browser flow. + # Client validates JWT tokens issued by this provider using the public key. + "client_secret_source": ("llm-serving", "local-llm-jwt", "client-secret"), + "generate_if_missing": True, + "extra_secret_literals": {"client-id": "local-llm"}, + "redirect_uris": [], # No browser flow, JWT/service-to-service only + "launch_url": "https://llm.riotpiao.com", + "display_name": "Local LLM", + }, } app_pks_for_binding = [] @@ -646,7 +656,7 @@ for name, app_pk in app_pks_for_binding: # Per-service admin groups are app-scoped (unlike homelab-admins' blanket # binding above) - only grants visibility/access to that one application. -# portainer/kmsvc/temporal/llm-serving have no Authentik Application (no OIDC +# portainer/kmsvc/temporal have no Authentik Application (no OIDC # login integration), so their groups exist for the "permissions" claim / # future k8s RBAC only - nothing to bind here. SERVICE_GROUP_TO_APP_SLUG = { @@ -656,6 +666,7 @@ SERVICE_GROUP_TO_APP_SLUG = { "homarr-admins": "homarr", "paperless-admins": "paperless", "immich-admins": "immich", + "llm-admins": "local-llm", } for group_name, app_slug in SERVICE_GROUP_TO_APP_SLUG.items(): app_pk = next((pk for n, pk in app_pks_for_binding if n == app_slug), None) @@ -674,6 +685,19 @@ for group_name, app_slug in SERVICE_GROUP_TO_APP_SLUG.items(): ) print(f" {app_slug}: {group_name} bound") +# JWT configuration for local-llm +print("\n[JWT] Fetching Authentik signing key for local-llm...") +status, signing_key_res = api("GET", f"/api/v3/crypto/certificatekeypairs/{SIGNING_KEY_PK}/") +if status == 200: + jwt_cert = signing_key_res.get("certificate", "") + print(f" Public certificate available for JWT validation (base64-encoded below)\n") + import base64 + cert_b64 = base64.b64encode(jwt_cert.encode()).decode() + print(f"Save this to local-llm config for JWT token validation:") + print(f" AUTHENTIK_JWT_CERT={cert_b64}") + print(f"\nJWT issuer URL: https://authentik.riotpiao.com/application/o/local-llm/") + print(f"Local-LLM credentials are stored in: kubectl -n llm-serving get secret local-llm-jwt") + print("\nDone. Summary:") print(" groups: homelab-admins (superuser) + " + ", ".join(SERVICE_ADMIN_GROUP_NAMES)) print(" user: rock -> homelab-admins + all service admin groups")