Sign in through an OpenID Connect provider, and from a terminal
terdut can now sign people in through any OIDC provider (written against Authentik), and let groups at the provider decide who may sign in, which teams they belong to and whether they administer the install. Password login keeps working alongside it; TERDUT_PASSWORD_LOGIN=false turns it off, and is refused at startup unless SSO is configured. With no TERDUT_OIDC_* setting nothing changes, so every existing install behaves as before. Identity is (issuer, subject), never email or username: those are mutable at the provider and a recycled address must not inherit an account. An existing user is linked by email only when the provider marks it verified, or TERDUT_OIDC_TRUST_EMAIL is set, which Authentik needs. Group grants are marked source='oidc' on team_members and users, and the sync changes only those rows. Hand-made memberships and administrators are left alone, and the sync bypasses the last-owner and last-admin guards because the provider is the source of truth for what it grants. Editing managed access by hand is refused with 409, since the next sign-in would undo it. The web UI badges it as SSO and disables the controls. Groups are read only at sign-in, so an SSO session carries a hard ceiling (sessions.max_expires_at, 12h by default) that sliding never extends. There is no refresh token, which means API keys of somebody removed at the provider stay valid until an administrator disables the user. That is accepted and documented, not fixed. A client with no browser, the TUI over SSH, signs in with a device code run by terdut itself (POST /api/oidc/device and /device/token), so the terminal never talks to the provider and ends up with the ordinary terdut_session cookie. Only a browser session can approve a code; an API key cannot. /device?code= sends a signed-out visitor through sign-in and back, which is what oidc_logins.next is for. oauth2 is pinned to v0.36.0: v0.37 needs Go 1.26 and the Dockerfile builds on 1.25. Migrations 011 and 012 add tables and defaulted columns only.
This commit is contained in:
@@ -61,8 +61,6 @@ spec:
|
||||
value: "{{ .Values.notify.fallbackTopic }}"
|
||||
- name: TERDUT_NOTIFY_REPEAT
|
||||
value: "{{ .Values.notify.repeatEvery }}"
|
||||
- name: TERDUT_PUBLIC_URL
|
||||
value: "{{ .Values.notify.publicUrl | default (printf "https://%s" .Values.networking.hostname) }}"
|
||||
{{- if .Values.notify.tokenSecret.name }}
|
||||
- name: TERDUT_NTFY_TOKEN
|
||||
valueFrom:
|
||||
@@ -71,6 +69,49 @@ spec:
|
||||
key: {{ .Values.notify.tokenSecret.key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
# Set whether or not ntfy is: single sign-on builds its redirect URI
|
||||
# from it, and sessions use it to decide the cookie's Secure flag.
|
||||
- name: TERDUT_PUBLIC_URL
|
||||
value: "{{ .Values.notify.publicUrl | default (printf "https://%s" .Values.networking.hostname) }}"
|
||||
- name: TERDUT_PASSWORD_LOGIN
|
||||
value: {{ .Values.passwordLogin | quote }}
|
||||
{{- if .Values.oidc.enabled }}
|
||||
- name: TERDUT_OIDC_ISSUER
|
||||
value: {{ required "oidc.issuer is required when oidc.enabled" .Values.oidc.issuer | quote }}
|
||||
- name: TERDUT_OIDC_CLIENT_ID
|
||||
value: {{ required "oidc.clientId is required when oidc.enabled" .Values.oidc.clientId | quote }}
|
||||
- name: TERDUT_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ required "oidc.clientSecret.name is required when oidc.enabled" .Values.oidc.clientSecret.name }}
|
||||
key: {{ .Values.oidc.clientSecret.key }}
|
||||
- name: TERDUT_OIDC_NAME
|
||||
value: {{ .Values.oidc.name | quote }}
|
||||
- name: TERDUT_OIDC_SCOPES
|
||||
value: {{ .Values.oidc.scopes | quote }}
|
||||
- name: TERDUT_OIDC_USERNAME_CLAIM
|
||||
value: {{ .Values.oidc.usernameClaim | quote }}
|
||||
- name: TERDUT_OIDC_EMAIL_CLAIM
|
||||
value: {{ .Values.oidc.emailClaim | quote }}
|
||||
- name: TERDUT_OIDC_GROUPS_CLAIM
|
||||
value: {{ .Values.oidc.groupsClaim | quote }}
|
||||
- name: TERDUT_OIDC_TRUST_EMAIL
|
||||
value: {{ .Values.oidc.trustEmail | quote }}
|
||||
- name: TERDUT_OIDC_SESSION_MAX_AGE
|
||||
value: {{ .Values.oidc.sessionMaxAge | quote }}
|
||||
{{- if .Values.oidc.allowedGroups }}
|
||||
- name: TERDUT_OIDC_ALLOWED_GROUPS
|
||||
value: {{ join "," .Values.oidc.allowedGroups | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.oidc.adminGroup }}
|
||||
- name: TERDUT_OIDC_ADMIN_GROUP
|
||||
value: {{ .Values.oidc.adminGroup | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.oidc.groupMappings }}
|
||||
- name: TERDUT_OIDC_GROUP_MAPPINGS
|
||||
value: {{ .Values.oidc.groupMappings | toJson | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
|
||||
@@ -107,6 +107,56 @@ notify:
|
||||
name: ""
|
||||
key: token
|
||||
|
||||
# Whether a user may sign in, or sign up, with a password. Turn it off once
|
||||
# single sign-on works, to make it the only way in; turn it back on (and
|
||||
# redeploy) if the identity provider is down and somebody has to get in.
|
||||
passwordLogin: true
|
||||
|
||||
# Single sign-on through an OpenID Connect provider such as Authentik.
|
||||
#
|
||||
# At the provider, create an OAuth2/OpenID application whose redirect URI is
|
||||
# <notify.publicUrl>/api/oidc/callback
|
||||
# (publicUrl defaults to https://<networking.hostname>), a confidential client, and
|
||||
# put the client secret in an existing Secret named by clientSecret below.
|
||||
#
|
||||
# Groups from the provider decide what a person can do. Access it grants is
|
||||
# marked as managed by single sign-on and is re-read at every sign-in; anything
|
||||
# added by hand in terdut is left alone. Changes in the provider take effect at
|
||||
# the person's next sign-in, at most sessionMaxAge later. API keys are NOT
|
||||
# revoked when somebody is removed at the provider: disable the user in terdut too.
|
||||
oidc:
|
||||
enabled: false
|
||||
# Issuer URL. For Authentik: https://<authentik>/application/o/<app-slug>/
|
||||
issuer: ""
|
||||
clientId: ""
|
||||
clientSecret:
|
||||
name: ""
|
||||
key: client-secret
|
||||
# What the sign-in button calls the provider.
|
||||
name: SSO
|
||||
# Authentik puts the groups claim behind the profile scope.
|
||||
scopes: "openid profile email"
|
||||
usernameClaim: preferred_username
|
||||
emailClaim: email
|
||||
groupsClaim: groups
|
||||
# Link a first sign-in to an existing local user with the same email even when
|
||||
# the provider does not mark the address verified. Authentik reports
|
||||
# email_verified as false unless configured otherwise.
|
||||
trustEmail: false
|
||||
# Only people in one of these groups may sign in. Empty admits everybody the
|
||||
# provider authenticates, and access control is left to the provider.
|
||||
allowedGroups: []
|
||||
# Members of this group are system administrators.
|
||||
adminGroup: ""
|
||||
# Team roles from groups. A team that does not exist is created. Where several
|
||||
# groups grant the same team the highest role wins.
|
||||
# - group: sre
|
||||
# team: SRE
|
||||
# role: member # member or owner
|
||||
groupMappings: []
|
||||
# Hard ceiling on a session made by a single sign-on login.
|
||||
sessionMaxAge: 12h
|
||||
|
||||
# Backups are no longer this chart's business. The SQLite database lived on a PVC
|
||||
# beside the app, so it needed a sidecar with a sqlite3 module for k8up to exec a
|
||||
# dump in; Postgres is backed up where it runs, through a k8up.io/backupcommand
|
||||
|
||||
Reference in New Issue
Block a user