Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -97,3 +97,56 @@ 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.<baseDomain>` 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.<baseDomain>/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=<baseDomain>
CLIENT_ID=<authentik.oauthApp.clientId>

# 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 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/" \
-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":"<fullName>","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.
23 changes: 22 additions & 1 deletion templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,34 @@ 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
*/}}
{{- define "envoy.jwksUri" -}}
{{- 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 -}}
7 changes: 5 additions & 2 deletions templates/authentik-blueprints-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -73,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
Expand Down Expand Up @@ -107,10 +108,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 }}
7 changes: 7 additions & 0 deletions templates/envoy/client-traffic-policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
3 changes: 3 additions & 0 deletions templates/envoy/security-policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ spec:
- name: authentik
remoteJWKS:
uri: {{ include "envoy.jwksUri" . }}
{{- with (include "envoy.jwtIssuer" .) }}
issuer: {{ . }}
{{- end }}
recomputeRoute: true
claimToHeaders:
- claim: sub
Expand Down
2 changes: 1 addition & 1 deletion templates/openwebui/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -137,5 +139,3 @@ authentik:
clientSecret:
accessTokenValidity: "days=30"
refreshTokenValidity: "days=365"
serviceAccount:
create: false
Loading