From 9cde42e4576e8d3d17214412ad03acd31d694e90 Mon Sep 17 00:00:00 2001 From: cmdoret Date: Fri, 21 Aug 2026 16:22:47 +0200 Subject: [PATCH 1/6] fix(envoy): strip header + set issuer --- templates/envoy/client-traffic-policy.yaml | 7 +++++++ templates/envoy/security-policy.yaml | 3 +++ 2 files changed, 10 insertions(+) diff --git a/templates/envoy/client-traffic-policy.yaml b/templates/envoy/client-traffic-policy.yaml index 0890ab0..44357d7 100644 --- a/templates/envoy/client-traffic-policy.yaml +++ b/templates/envoy/client-traffic-policy.yaml @@ -13,4 +13,11 @@ spec: name: {{ include "envoy.fullname" . }} connection: bufferLimit: {{ .Values.envoy.clientBufferLimit | default "50Mi" }} + {{- /* A client-set x-sub would outrank the verified claim: jwt_authn appends, and + readers take the first value. Mirror claimToHeaders in security-policy.yaml. */}} + headers: + earlyRequestHeaders: + remove: + - x-sub + - x-name {{- end }} diff --git a/templates/envoy/security-policy.yaml b/templates/envoy/security-policy.yaml index d535e75..51714d6 100644 --- a/templates/envoy/security-policy.yaml +++ b/templates/envoy/security-policy.yaml @@ -14,6 +14,9 @@ spec: - name: authentik remoteJWKS: uri: {{ include "envoy.jwksUri" . }} + {{- with (include "envoy.jwtIssuer" .) }} + issuer: {{ . }} + {{- end }} recomputeRoute: true claimToHeaders: - claim: sub From 0e8edaf1cc16f52f2d993ce09924c04d6511482e Mon Sep 17 00:00:00 2001 From: cmdoret Date: Fri, 21 Aug 2026 16:23:12 +0200 Subject: [PATCH 2/6] feat: add issuer value --- values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/values.yaml b/values.yaml index d891bf3..a09c54d 100644 --- a/values.yaml +++ b/values.yaml @@ -5,6 +5,8 @@ envoy: clientBufferLimit: 50Mi security: jwksUri: + # `iss` to require. Derived from authentik by default; required when jwksUri is set. + issuer: gatewayClass: enabled: false name: @@ -137,5 +139,3 @@ authentik: clientSecret: accessTokenValidity: "days=30" refreshTokenValidity: "days=365" - serviceAccount: - create: false From 59fc4d3cba458ee4548eb7af05869a7d997135ec Mon Sep 17 00:00:00 2001 From: cmdoret Date: Fri, 21 Aug 2026 16:25:13 +0200 Subject: [PATCH 3/6] feat: issuer helpers --- templates/_helpers.tpl | 23 ++++++++++++++++++++++- templates/openwebui/deployment.yaml | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index e2cd203..8cd97b3 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -37,6 +37,15 @@ Usage: {{ include "model.fullname" (merge (dict "modelName" $name) $) }} {{- printf "%s-model-%s" .Release.Name .modelName -}} {{- end -}} +{{/* +Base URL of this release's authentik OAuth2 provider, with a trailing slash. +Callers append the endpoint they need; authentik issues per-provider URLs, so this +prefix is also the `iss` claim on the tokens it signs. +*/}} +{{- define "authentik.providerUrl" -}} + {{- printf "https://authentik.%s/application/o/%s/" .Values.envoy.baseDomain .Values.authentik.oauthApp.name -}} +{{- end -}} + {{/* JWKS URI: use authentik if enabled, otherwise configurable */}} @@ -44,6 +53,18 @@ JWKS URI: use authentik if enabled, otherwise configurable {{- if .Values.envoy.security.jwksUri -}} {{ .Values.envoy.security.jwksUri }} {{- else if .Values.authentik.enabled -}} - {{- printf "https://authentik.%s/application/o/%s/jwks/" .Values.envoy.baseDomain .Values.authentik.oauthApp.name -}} + {{- printf "%sjwks/" (include "authentik.providerUrl" .) -}} + {{- end -}} +{{- end -}} + +{{/* +Expected `iss` claim. One authentik signs every provider with the same key, so the +JWKS alone would also accept another application's tokens. +*/}} +{{- define "envoy.jwtIssuer" -}} + {{- if .Values.envoy.security.jwksUri -}} + {{- .Values.envoy.security.issuer | required "envoy.security.issuer is required alongside envoy.security.jwksUri, or the gateway accepts every token that JWKS validates" -}} + {{- else if .Values.authentik.enabled -}} + {{- include "authentik.providerUrl" . -}} {{- end -}} {{- end -}} diff --git a/templates/openwebui/deployment.yaml b/templates/openwebui/deployment.yaml index f472915..30faa01 100644 --- a/templates/openwebui/deployment.yaml +++ b/templates/openwebui/deployment.yaml @@ -61,7 +61,7 @@ spec: - name: OAUTH_ADMIN_ROLES value: gateway admins - name: OPENID_PROVIDER_URL - value: https://authentik.{{ .Values.envoy.baseDomain }}/application/o/{{ .Values.authentik.oauthApp.name }}/.well-known/openid-configuration + value: {{ include "authentik.providerUrl" . }}.well-known/openid-configuration - name: OPENID_REDIRECT_URI value: https://openwebui.{{ .Values.envoy.baseDomain }}/oauth/oidc/callback - name: OAUTH_PROVIDER_NAME From 33128e0ee246ed836322050bec040d13a0ad6e2b Mon Sep 17 00:00:00 2001 From: cmdoret Date: Fri, 21 Aug 2026 16:27:39 +0200 Subject: [PATCH 4/6] fix(authentik): make brand default --- templates/authentik-blueprints-secret.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/authentik-blueprints-secret.yaml b/templates/authentik-blueprints-secret.yaml index da0d0ba..bd7b64d 100644 --- a/templates/authentik-blueprints-secret.yaml +++ b/templates/authentik-blueprints-secret.yaml @@ -41,7 +41,6 @@ stringData: name: vllm-service-account is_active: true type: service_account - # OAuth2 Provider (depends on: certificatekeypair #1) - model: authentik_providers_oauth2.oauth2provider identifiers: @@ -107,10 +106,12 @@ stringData: sources: - !Find [authentik_sources_oauth.oauthsource, [slug, datascience-gitlab]] - # Brand (depends on: default-device-code flow, which is a built-in flow) + # Authentik picks a brand whose domain matches the request host or which is flagged default + # and synthesises an unconfigured in-memory brand when neither matches. - model: authentik_brands.brand identifiers: domain: authentik-default attrs: + default: true flow_device_code: !Find [authentik_flows.flow, [slug, default-source-pre-authentication]] {{- end }} From 6332ebf6e8098fb5f7b3a8d1b79f99e4168f6cbd Mon Sep 17 00:00:00 2001 From: cmdoret Date: Fri, 21 Aug 2026 16:36:39 +0200 Subject: [PATCH 5/6] doc(readme): api usage --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 23525c8..26d9fae 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The chart only creates custom resources that rely on these systems being install - [cert-manager](https://cert-manager.io/) with a `ClusterIssuer` matching `envoy.clusterissuer` - A PostgreSQL server, with roles and databases created up front. See [postgresql.md](docs/postgresql.md) -## Usage +## Installation The repository contains a [`justfile`](justfile) to automate routine commands. You may use it as reference, or run it with `just` (by default, just will list available recipes). @@ -97,3 +97,55 @@ just to make sure the user is created in openwebui. click on the user icon in the bottom left, go to "Admin Panel" -> "Settings" -> "Models". For each model, click on the Pen icon to edit, then the "Access" button in the top right. Set to "Public", close and "save". This has to be done each time models are changed. + +## Usage + +### Web interface + +Open `https://openwebui.` and sign in with the "authentik" button, which +delegates to GitLab. The first sign-in creates the account; accounts are matched by +email, so signing in a different way later does not create a duplicate. + +Members of the `gateway admins` group in authentik become OpenWebUI admins. + +Newly added models stay private until an admin makes them public once, see +[Manual installation steps](#manual-installation-steps). + +A model that has scaled to zero takes a minute or two to answer the first message. + +### API access + +The gateway at `https://gateway./v1` speaks the OpenAI API and takes an +authentik access token as its bearer credential. `GET /v1/models` lists the model names to +use; they are the `models.*.fullName` values. + +Get a token with the device code flow. `CLIENT_ID` is `authentik.oauthApp.clientId` -- a +public client, so it is not a secret: + +```bash +DOMAIN= +CLIENT_ID= + +# 1. start the flow, then open verification_uri_complete in a browser and approve +curl -s "https://authentik.$DOMAIN/application/o/device/" \ + -d client_id="$CLIENT_ID" -d scope="openid profile email" | tee /tmp/dev.json | jq + +# 2. exchange the device code for a token (returns authorization_pending until approved) +TOKEN=$(curl -s "https://authentik.$DOMAIN/application/o/token/" \ + -d grant_type=urn:ietf:params:oauth:grant-type:device_code \ + -d client_id="$CLIENT_ID" \ + -d device_code="$(jq -r .device_code /tmp/dev.json)" | jq -r .access_token) + +curl "https://gateway.$DOMAIN/v1/chat/completions" \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"model":"","messages":[{"role":"user","content":"hello"}]}' +``` + +Or point any OpenAI client at it: +`OpenAI(base_url=f"https://gateway.{DOMAIN}/v1", api_key=TOKEN)`. + +The token is yours, so rate limits and usage are attributed to you. Access tokens last +`authentik.oauthApp.accessTokenValidity` (30 days by default); the device grant also +returns a refresh token, valid for `refreshTokenValidity`, so a client can renew without +a second browser approval. A model that has scaled to zero takes a minute or two to answer +the first request. From 862c73012d3dab735c31cd2a97b09e500b7a82eb Mon Sep 17 00:00:00 2001 From: cmdoret Date: Fri, 21 Aug 2026 16:51:20 +0200 Subject: [PATCH 6/6] fix(authentik): enable offline_access for refresh tokens --- README.md | 3 ++- templates/authentik-blueprints-secret.yaml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 26d9fae..956e4e3 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,8 @@ CLIENT_ID= # 1. start the flow, then open verification_uri_complete in a browser and approve curl -s "https://authentik.$DOMAIN/application/o/device/" \ - -d client_id="$CLIENT_ID" -d scope="openid profile email" | tee /tmp/dev.json | jq + -d client_id="$CLIENT_ID" -d scope="openid profile email offline_access" \ + | tee /tmp/dev.json | jq # 2. exchange the device code for a token (returns authorization_pending until approved) TOKEN=$(curl -s "https://authentik.$DOMAIN/application/o/token/" \ diff --git a/templates/authentik-blueprints-secret.yaml b/templates/authentik-blueprints-secret.yaml index bd7b64d..de0f452 100644 --- a/templates/authentik-blueprints-secret.yaml +++ b/templates/authentik-blueprints-secret.yaml @@ -72,6 +72,8 @@ stringData: - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] - !Find [authentik_providers_oauth2.scopemapping, [scope_name, entitlements]] + {{- /* Required for refresh tokens: refresh token are only issued when offline_access survives that. */}} + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] # OAuth2 Application (depends on: oauth2provider #5) - model: authentik_core.application