Roll the verified Option-B per-app oauth2-proxy sidecar out to the remaining four forward-auth apps after the pihole canary was confirmed fail-closed (unauthenticated -> 302 to Keycloak, pod 2/2, clean sidecar OIDC discovery). Each app's Gateway route now terminates at its oauth2-proxy sidecar (:4180, --allowed-group=media-users, --upstream=http://127.0.0.1:<uiport>); the gateway-ingress CiliumNetworkPolicy allows the Gateway to reach ONLY :4180, so the bare UI is never served un-authenticated (fail-closed; R-IDP-02/06). The apps' own app/API ports stay in-Service for EAST-WEST callers (arr -> qbittorrent:8080 / jackett:9117) via the existing reciprocal-ingress allows. - keycloak: confidential jackett/qbittorrent/radarr/sonarr clients (realm-import + realm-clients mirror), provisioned live via kcadm (groups membership mapper). - secrets: four SOPS/age <app>-oauth2-proxy-credentials, wired into apps/wave-0 and apps/wave-1 by the renderer's sidecar-creds wave rule. - ci: remove the temporary canary deferral (all five goldens now reproduce). Co-Authored-By: Claude Opus 4.8 <[email protected]>
602 lines
26 KiB
YAML
602 lines
26 KiB
YAML
---
|
|
# Identity config as code (R-IDP-03): the `homelab` realm — groups, roles, OIDC clients, brute-force
|
|
# hardening, and a conditional MFA flow that REQUIRES OTP for platform-admins (R-IDP-04). Reconciled
|
|
# declaratively by the Keycloak Operator; CARRIED BY platform-config (dependsOn platform).
|
|
#
|
|
# Client secrets are NOT in this file: they are injected from the SOPS-encrypted keycloak-realm-clients
|
|
# Secret via spec.placeholders ($(env:...)) — so secrets exist only encrypted (R-IDP-04; SOPS, docs/07).
|
|
# Until that Secret is filled (operator hand-off, see keycloak-realm-clients.sops.yaml.example), this
|
|
# import stays PENDING on the unresolved placeholder.
|
|
apiVersion: k8s.keycloak.org/v2alpha1
|
|
kind: KeycloakRealmImport
|
|
metadata:
|
|
name: homelab
|
|
namespace: keycloak
|
|
spec:
|
|
keycloakCRName: keycloak
|
|
placeholders:
|
|
GRAFANA_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: grafana-client-secret
|
|
OAUTH2_PROXY_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: oauth2-proxy-client-secret
|
|
GITEA_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: gitea-client-secret
|
|
NEXTCLOUD_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: nextcloud-client-secret
|
|
SEMAPHORE_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: semaphore-client-secret
|
|
HEADLAMP_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: headlamp-client-secret
|
|
CRAFTY_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: crafty-client-secret
|
|
PIHOLE_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: pihole-client-secret
|
|
JACKETT_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: jackett-client-secret
|
|
QBITTORRENT_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: qbittorrent-client-secret
|
|
RADARR_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: radarr-client-secret
|
|
SONARR_CLIENT_SECRET:
|
|
secret:
|
|
name: keycloak-realm-clients
|
|
key: sonarr-client-secret
|
|
realm:
|
|
realm: homelab
|
|
enabled: true
|
|
displayName: "Homelab SSO"
|
|
loginWithEmailAllowed: true
|
|
registrationAllowed: false
|
|
resetPasswordAllowed: true
|
|
# Hardening (docs/15): brute-force detection ON; SSL required for external (edge-TLS) requests.
|
|
bruteForceProtected: true
|
|
permanentLockout: false
|
|
sslRequired: external
|
|
# --- Authorization model (R-IDP-06): groups drive access; platform-admins carry a realm role. ---
|
|
roles:
|
|
realm:
|
|
- name: platform-admin
|
|
description: "Privileged platform administrators — MFA required (R-IDP-04)."
|
|
groups:
|
|
- name: platform-admins
|
|
realmRoles:
|
|
- platform-admin
|
|
- name: media-users
|
|
- name: viewers
|
|
# --- OIDC clients (native SSO). Group membership is exposed as a `groups` claim so apps and
|
|
# oauth2-proxy can authorize by group (R-IDP-06). ---
|
|
clients:
|
|
# Grafana — the concrete native-OIDC SSO proof target (docs/10 / deliverable #4).
|
|
- clientId: grafana
|
|
name: Grafana
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:GRAFANA_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://grafana.home.keithjackson.dev/login/generic_oauth
|
|
webOrigins:
|
|
- https://grafana.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# oauth2-proxy — forward-auth for non-OIDC UIs (PiHole, *arr, Prometheus, Alertmanager, …).
|
|
- clientId: oauth2-proxy
|
|
name: oauth2-proxy (forward-auth)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:OAUTH2_PROXY_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://*.home.keithjackson.dev/oauth2/callback
|
|
- https://auth.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://*.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# kubernetes — kubectl OIDC login via kubelogin (int128/kubelogin `oidc-login`). PUBLIC client +
|
|
# PKCE (S256) so NO client secret lives in any user's kubeconfig; kubelogin runs the auth-code
|
|
# +PKCE flow against a localhost callback and caches the ID token. The k3s API server validates
|
|
# that ID token (kube-apiserver oidc-issuer-url=this realm, oidc-client-id=kubernetes,
|
|
# oidc-username-claim=email, oidc-groups-claim=groups); RBAC binds the `groups` claim — see
|
|
# k8s-oidc-rbac.yaml (oidc:platform-admins -> cluster-admin, oidc:viewers -> view). The cert
|
|
# kubeconfig remains the break-glass admin, independent of Keycloak/OIDC availability.
|
|
- clientId: kubernetes
|
|
name: Kubernetes (kubectl)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: true
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
attributes:
|
|
"pkce.code.challenge.method": "S256"
|
|
redirectUris:
|
|
- http://localhost:8000
|
|
- http://localhost:8000/
|
|
- http://localhost:18000
|
|
- http://localhost:18000/
|
|
- http://127.0.0.1:8000
|
|
- http://127.0.0.1:8000/
|
|
- http://127.0.0.1:18000
|
|
- http://127.0.0.1:18000/
|
|
webOrigins:
|
|
- http://localhost
|
|
- http://127.0.0.1
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
# offline_access (optional): lets kubelogin request a long-lived OFFLINE refresh token via
|
|
# `--oidc-extra-scope=offline_access`, so the operator's kubectl isn't forced to re-auth on the
|
|
# ONLINE session idle/max timeouts (ssoSessionIdleTimeout 30m / ssoSessionMaxLifespan 10h).
|
|
# Offline tokens honour offlineSessionIdleTimeout (30d) with offlineSessionMaxLifespanEnabled=false
|
|
# (no hard cap) — see k8s-oidc-rbac.yaml header for the kubeconfig exec-arg. Optional (not default)
|
|
# so it is opt-in per the client's request scopes; realm session timeouts stay unchanged.
|
|
optionalClientScopes:
|
|
- offline_access
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# gitea — native OIDC SSO for the bootstrapping-critical git host (catalog/gitea.yaml auth.mode
|
|
# oidc; docs/13 SSO row, docs/15 "Native OIDC"; R-IDP-02). Confidential auth-code client modeled on
|
|
# grafana. Redirect URI is Gitea's OAuth2 callback for the auth source named `keycloak`
|
|
# (/user/oauth2/<source>/callback). The `groups` claim drives authorization: Gitea maps the
|
|
# platform-admins group -> Gitea admin (auth source --admin-group; R-IDP-06). The LOCAL Gitea admin
|
|
# remains the break-glass path (docs/15 availability caveat). Secret injected from the SOPS
|
|
# keycloak-realm-clients Secret via the GITEA_CLIENT_SECRET placeholder (R-IDP-04).
|
|
- clientId: gitea
|
|
name: Gitea
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:GITEA_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://gitea.home.keithjackson.dev/user/oauth2/keycloak/callback
|
|
webOrigins:
|
|
- https://gitea.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# nextcloud — native OIDC SSO for the file-sync host (catalog/nextcloud.yaml auth.mode oidc; docs/13
|
|
# SSO row, docs/15; R-IDP-02). Confidential auth-code client modeled on grafana. Redirect URI is the
|
|
# user_oidc app's callback (/apps/user_oidc/code). The `groups` claim drives group provisioning in
|
|
# Nextcloud (user_oidc --group-provisioning; R-IDP-06). The LOCAL Nextcloud admin stays break-glass
|
|
# (docs/15). Secret injected from the SOPS keycloak-realm-clients Secret via NEXTCLOUD_CLIENT_SECRET.
|
|
- clientId: nextcloud
|
|
name: Nextcloud
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:NEXTCLOUD_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://nextcloud.home.keithjackson.dev/apps/user_oidc/code
|
|
webOrigins:
|
|
- https://nextcloud.home.keithjackson.dev
|
|
# Post-logout redirect allow-list (#logout "redirect failed"). On sign-out user_oidc redirects the
|
|
# browser to Keycloak's end_session_endpoint with post_logout_redirect_uri=<Nextcloud base '/'> and
|
|
# client_id=nextcloud but NO id_token_hint (provider sendIdTokenHint=0), so Keycloak validates the
|
|
# redirect against this client attribute — empty => "Invalid redirect uri" => the operator's
|
|
# "redirect failed". The wildcard covers the base URL and any logout landing path. Keycloak stores
|
|
# this as the client attribute `post.logout.redirect.uris` (## separates multiple values).
|
|
attributes:
|
|
post.logout.redirect.uris: https://nextcloud.home.keithjackson.dev/*
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# semaphore — native OIDC SSO for the Ansible UI (catalog/semaphore.yaml auth.mode oidc; docs/13 SSO
|
|
# row, docs/15; R-IDP-02). Confidential auth-code client modeled on grafana. Redirect URI is
|
|
# Semaphore's OIDC callback (/api/auth/oidc/<provider>/redirect for provider `keycloak`). The
|
|
# `groups` claim is exposed for authorization (R-IDP-06). The LOCAL Semaphore admin stays break-glass
|
|
# (docs/15). Secret injected from the SOPS keycloak-realm-clients Secret via SEMAPHORE_CLIENT_SECRET.
|
|
- clientId: semaphore
|
|
name: Semaphore
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:SEMAPHORE_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://semaphore.home.keithjackson.dev/api/auth/oidc/keycloak/redirect
|
|
webOrigins:
|
|
- https://semaphore.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# headlamp — the Kubernetes dashboard (platform/headlamp). Confidential auth-code client modeled
|
|
# on grafana. Redirect URI is Headlamp's OIDC callback (<base>/oidc-callback). Headlamp forwards
|
|
# the logged-in user's id_token to the k3s apiserver as the bearer, so RBAC is applied PER USER
|
|
# via the existing oidc:* group bindings (k8s-oidc-rbac.yaml) — NOT via Headlamp's ServiceAccount.
|
|
#
|
|
# AUDIENCE MAPPER (the key integration nuance): the apiserver is configured oidc-client-id=
|
|
# kubernetes, so it only accepts ID tokens whose `aud` CONTAINS `kubernetes`. A plain `headlamp`
|
|
# client issues aud=[headlamp] -> the apiserver would REJECT it. The oidc-audience-mapper below
|
|
# ADDS the `kubernetes` client to the audience, giving aud=[headlamp, kubernetes]: Headlamp's own
|
|
# verifier accepts it (aud contains headlamp) AND the apiserver accepts it (aud contains
|
|
# kubernetes). This keeps the apiserver's oidc config untouched (preferred over widening
|
|
# api-audiences). The `groups` mapper (same as the other clients) carries the group claim the
|
|
# apiserver maps to oidc:<group> for the RBAC bindings.
|
|
- clientId: headlamp
|
|
name: Headlamp (Kubernetes dashboard)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:HEADLAMP_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://headlamp.home.keithjackson.dev/oidc-callback
|
|
webOrigins:
|
|
- https://headlamp.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# Add `kubernetes` to the token audience so the k3s apiserver (oidc-client-id=kubernetes)
|
|
# accepts the forwarded id_token. `included.client.audience` names the existing `kubernetes`
|
|
# client; the mapper appends it to `aud` in the ID token (and access token, harmless).
|
|
- name: audience-kubernetes
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-audience-mapper
|
|
config:
|
|
"included.client.audience": kubernetes
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
# crafty — the Minecraft management panel (platform/crafty). Crafty's web UI has NO native OIDC,
|
|
# so it is gated by a PER-APP oauth2-proxy REVERSE-PROXY sidecar (Option B; platform/crafty +
|
|
# platform/oauth2-proxy/README.md) — this is that sidecar's confidential auth-code client, modeled
|
|
# on the grafana/oauth2-proxy clients. Redirect URI is the oauth2-proxy callback on Crafty's host.
|
|
# The `groups` claim (bare names) lets the sidecar enforce --allowed-groups=platform-admins
|
|
# (R-IDP-06). Secret injected from the SOPS keycloak-realm-clients Secret via CRAFTY_CLIENT_SECRET
|
|
# (R-IDP-04); the SAME value is mirrored in platform/crafty/crafty-oauth2-proxy-credentials.sops.yaml
|
|
# (cross-namespace mirror, like grafana/gitea/headlamp). Provisioned live via kcadm at authoring
|
|
# time (2026-08-28) and captured here as the durable GitOps source of truth.
|
|
- clientId: crafty
|
|
name: Crafty Controller (oauth2-proxy)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:CRAFTY_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://crafty.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://crafty.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# pihole — the DNS admin UI (catalog/pihole.yaml auth.mode forward-auth). PiHole's UI has NO
|
|
# native OIDC, so it is gated by a PER-APP oauth2-proxy REVERSE-PROXY sidecar (Option B, rendered
|
|
# by catalog/render.py) — this is that sidecar's confidential auth-code client, modeled on the
|
|
# crafty/oauth2-proxy clients. Redirect URI is the oauth2-proxy callback on PiHole's host. The
|
|
# `groups` claim (bare names) lets the sidecar enforce --allowed-group=platform-admins (R-IDP-06).
|
|
# Secret injected from the SOPS keycloak-realm-clients Secret via PIHOLE_CLIENT_SECRET (R-IDP-04);
|
|
# the SAME value is mirrored in apps/wave-0/pihole-oauth2-proxy-credentials.sops.yaml (cross-
|
|
# namespace mirror). Provisioned live via kcadm at authoring time (2026-08-31) and captured here
|
|
# as the durable GitOps source of truth (the operator imports the realm once; kcadm is authoritative).
|
|
- clientId: pihole
|
|
name: PiHole (oauth2-proxy)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:PIHOLE_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://pihole.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://pihole.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# jackett/qbittorrent/radarr/sonarr — media stack UIs (catalog/*.yaml auth.mode forward-auth,
|
|
# auth.groups media-users). Each has a bare UI with no usable native OIDC, gated by a per-app
|
|
# oauth2-proxy REVERSE-PROXY sidecar (Option B, rendered by catalog/render.py) — these are those
|
|
# sidecars' confidential auth-code clients, modeled on the crafty/pihole clients. Redirect URI is
|
|
# the oauth2-proxy callback on each app's host; the `groups` mapper (bare names) lets the sidecar
|
|
# enforce --allowed-group=media-users (R-IDP-06). Secrets injected from the SOPS keycloak-realm-
|
|
# clients Secret via the *_CLIENT_SECRET placeholders (R-IDP-04); the SAME values are mirrored in
|
|
# apps/wave-*/<app>-oauth2-proxy-credentials.sops.yaml. Provisioned live via kcadm 2026-08-31 and
|
|
# captured here as the durable GitOps source of truth (operator imports once; kcadm authoritative).
|
|
- clientId: jackett
|
|
name: Jackett (oauth2-proxy)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:JACKETT_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://jackett.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://jackett.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
- clientId: qbittorrent
|
|
name: qBittorrent (oauth2-proxy)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:QBITTORRENT_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://qbittorrent.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://qbittorrent.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
- clientId: radarr
|
|
name: Radarr (oauth2-proxy)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:RADARR_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://radarr.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://radarr.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
- clientId: sonarr
|
|
name: Sonarr (oauth2-proxy)
|
|
protocol: openid-connect
|
|
enabled: true
|
|
publicClient: false
|
|
standardFlowEnabled: true
|
|
directAccessGrantsEnabled: false
|
|
secret: "$(env:SONARR_CLIENT_SECRET)"
|
|
redirectUris:
|
|
- https://sonarr.home.keithjackson.dev/oauth2/callback
|
|
webOrigins:
|
|
- https://sonarr.home.keithjackson.dev
|
|
defaultClientScopes:
|
|
- email
|
|
- profile
|
|
- roles
|
|
protocolMappers:
|
|
- name: groups
|
|
protocol: openid-connect
|
|
protocolMapper: oidc-group-membership-mapper
|
|
config:
|
|
"full.path": "false"
|
|
"id.token.claim": "true"
|
|
"access.token.claim": "true"
|
|
"userinfo.token.claim": "true"
|
|
"claim.name": groups
|
|
# --- MFA for platform-admins (R-IDP-04): a browser flow whose OTP step is CONDITIONAL on the
|
|
# platform-admin role. Non-admins log in without OTP; platform-admins must enroll/present TOTP.
|
|
# Set as the realm browserFlow below. VERIFY at bring-up (see runbook): a malformed flow fails
|
|
# the whole import — if so, the fallback is to comment this block + browserFlow and configure
|
|
# the conditional-OTP-by-role flow via the admin console. ---
|
|
authenticationFlows:
|
|
- alias: browser-mfa
|
|
description: "Browser flow with conditional OTP required for platform-admins."
|
|
providerId: basic-flow
|
|
topLevel: true
|
|
builtIn: false
|
|
authenticationExecutions:
|
|
- authenticator: auth-cookie
|
|
authenticatorFlow: false
|
|
requirement: ALTERNATIVE
|
|
priority: 10
|
|
userSetupAllowed: false
|
|
- authenticator: identity-provider-redirector
|
|
authenticatorFlow: false
|
|
requirement: ALTERNATIVE
|
|
priority: 20
|
|
userSetupAllowed: false
|
|
- authenticatorFlow: true
|
|
requirement: ALTERNATIVE
|
|
priority: 30
|
|
flowAlias: browser-mfa-forms
|
|
userSetupAllowed: false
|
|
- alias: browser-mfa-forms
|
|
description: "Username/password then conditional OTP."
|
|
providerId: basic-flow
|
|
topLevel: false
|
|
builtIn: false
|
|
authenticationExecutions:
|
|
- authenticator: auth-username-password-form
|
|
authenticatorFlow: false
|
|
requirement: REQUIRED
|
|
priority: 10
|
|
userSetupAllowed: false
|
|
- authenticatorFlow: true
|
|
requirement: CONDITIONAL
|
|
priority: 20
|
|
flowAlias: browser-mfa-conditional-otp
|
|
userSetupAllowed: false
|
|
- alias: browser-mfa-conditional-otp
|
|
description: "Require OTP only for users with the platform-admin role."
|
|
providerId: basic-flow
|
|
topLevel: false
|
|
builtIn: false
|
|
authenticationExecutions:
|
|
- authenticator: conditional-user-role
|
|
authenticatorFlow: false
|
|
requirement: REQUIRED
|
|
priority: 10
|
|
authenticatorConfig: conditional-otp-platform-admin
|
|
userSetupAllowed: false
|
|
- authenticator: auth-otp-form
|
|
authenticatorFlow: false
|
|
requirement: REQUIRED
|
|
priority: 20
|
|
userSetupAllowed: false
|
|
authenticatorConfig:
|
|
- alias: conditional-otp-platform-admin
|
|
config:
|
|
# Keycloak's conditional-user-role authenticator reads the role from `condUserRole`
|
|
# (ConditionalUserRoleAuthenticatorFactory.CONDITIONAL_USER_ROLE). The prior key
|
|
# "condition.evaluated.user.role" is not a real property, so the role resolved to null →
|
|
# "Invalid role name submitted: null" → the REQUIRED conditional-OTP step threw and blocked
|
|
# every platform-admins login (e.g. Headlamp) with a 403. Value is the realm role name.
|
|
condUserRole: platform-admin
|
|
negate: "false"
|
|
browserFlow: browser-mfa
|