From 5fd465824ec15aaf82a7bd5313597ece0b773a4c Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Thu, 2 Jul 2026 00:13:12 +0300 Subject: [PATCH 1/3] docs(monitoring): add OIDC authentication guide for Grafana Guide covers spec.oidc modes (None/System/CustomConfig) on the Monitoring CR, how System mode provisions a per-instance Keycloak client + audience scope + three role-mapping groups in the cozy realm, and how a user logs in through the Grafana browser flow. Placed under docs/next/operations/services/monitoring/ (weight 5, between setup and dashboards); ships alongside the code PR that adds the spec.oidc selector to the Monitoring chart. Signed-off-by: IvanHunters --- .../monitoring/oidc-authentication.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 content/en/docs/next/operations/services/monitoring/oidc-authentication.md diff --git a/content/en/docs/next/operations/services/monitoring/oidc-authentication.md b/content/en/docs/next/operations/services/monitoring/oidc-authentication.md new file mode 100644 index 00000000..34cbd77f --- /dev/null +++ b/content/en/docs/next/operations/services/monitoring/oidc-authentication.md @@ -0,0 +1,132 @@ +--- +title: "OIDC authentication for Grafana" +linkTitle: "OIDC authentication" +description: "Give tenant users per-identity Grafana access with per-instance audience isolation." +weight: 5 +--- + +Cozystack Grafana instances can authenticate users through OIDC instead of the shared `admin_user` / `admin_password` Secret. Each user then has their own identity, per-user audit, and a role that can be revoked by removing them from a Keycloak group. + +The identity model is deliberately **per-instance** rather than per-tenant: each Monitoring release (per-tenant `monitoring`, plus the platform's `monitoring-system`) gets its own OIDC audience, and a token minted for instance A is rejected by instance B's Grafana. That gives cross-tenant isolation without provisioning a Keycloak realm per tenant. The full rationale (why per-cluster audience and not per-tenant realm; how it relates to Keycloak Organizations; what BYO-OIDC looks like) is in the [design proposal](https://github.com/cozystack/community/pull/24). The tenant kube-apiserver's Phase 1 ([cozystack/cozystack#3044](https://github.com/cozystack/cozystack/pull/3044)) uses the same shape; this Grafana integration is the Phase-1 follow-up called out in that PR's body. + +{{% alert color="info" %}} +The `grafana-admin-password` Secret in the tenant namespace stays available as a break-glass path regardless of whether OIDC is enabled. `disable_login_form` is not flipped by the selector. +{{% /alert %}} + +## Modes + +`spec.oidc.mode` picks the identity source on the `Monitoring` CR: + +- **`None`** — the default. No OIDC; only the `admin_user` / `admin_password` Secret works. Existing instances render identically to before. +- **`System`** — trust the platform `cozy` Keycloak realm via a per-instance confidential client and audience binding. Users are the ones a Cozystack platform admin already provisioned in `cozy`; the tenant does not manage a directory of its own. +- **`CustomConfig`** — trust a tenant-supplied issuer directly (BYO IdP: Okta, Auth0, a customer's own Keycloak). `cozy` is not in the path. + +## Enable OIDC — `System` mode + +```yaml +apiVersion: apps.cozystack.io/v1alpha1 +kind: Monitoring +metadata: + name: monitoring + namespace: tenant-acme +spec: + oidc: + mode: System + # ... +``` + +Cozystack provisions: + +- A per-instance `KeycloakClient` in the `cozy` realm with `clientId` set to `-` (for the CR above: `tenant-acme-monitoring`). `public: false`, `directAccess: false`, `secret` sourced from a chart-owned Kubernetes Secret. `redirectUris` locked to `https://grafana./login/generic_oauth`. +- A per-instance `KeycloakClientScope` whose audience mapper pins the token's `aud` claim to that same `clientId` — the isolation primitive. +- Three `KeycloakRealmGroup` objects — `--admin`, `-editor`, `-viewer`. The chart owns the groups; membership is managed out-of-band by a Keycloak operator. +- A persistent Kubernetes Secret carrying the confidential `client-secret` (random on first install, preserved on upgrades). +- The Grafana CR's `spec.config.auth.generic_oauth` section wired to the cozy realm issuer, per-instance audience scope, and a `role_attribute_path` that maps the three groups above to Grafana's `Admin` / `Editor` / `Viewer` roles. + +The platform Grafana release (`monitoring-system` in the `cozy-monitoring` namespace) additionally gets `allow_assign_grafana_admin: true` so an `Admin`-group member is auto-promoted to server-level `GrafanaAdmin`. Tenant Grafana instances stay at org-level Admin. + +### Prerequisite + +`System` mode requires the platform-level OIDC feature (`authentication.oidc.enabled` at the Cozystack platform values). If the flag is off, the chart hard-fails the render with a clear message. Ask a Cozystack platform admin to enable it, or use `CustomConfig`. + +## Enable OIDC — `CustomConfig` mode + +Bring your own issuer. Two supply paths, **mutually exclusive**: + +```yaml +spec: + oidc: + mode: CustomConfig + customConfig: + config: + client_id: my-grafana + client_secret: xxxxxxxx + auth_url: https://idp.acme.example/protocol/openid-connect/auth + token_url: https://idp.acme.example/protocol/openid-connect/token + api_url: https://idp.acme.example/protocol/openid-connect/userinfo + scopes: openid email profile groups + role_attribute_path: "contains(groups[*], 'grafana-admins') && 'Admin' || 'Viewer'" +``` + +…or via a pre-existing Secret in the tenant namespace holding a ready-made `[auth.generic_oauth]` ini fragment in the `auth.ini` key: + +```yaml +spec: + oidc: + mode: CustomConfig + customConfig: + secretRef: + name: acme-byo-grafana-auth +``` + +Setting both `config` and `secretRef.name` (or neither) fails the render. In `CustomConfig` mode no Keycloak objects are provisioned in `cozy`; the Grafana instance trusts the operator-supplied issuer directly. + +## Assigning roles + +Grafana has three org-level roles — `Admin`, `Editor`, `Viewer` — and the chart drives them via `role_attribute_path`: + +```text +contains(groups[*], '--admin') && 'Admin' || +contains(groups[*], '--editor') && 'Editor' || +contains(groups[*], '--viewer') && 'Viewer' || +'Viewer' +``` + +Authenticated users with none of the three groups default to `Viewer`. To give a user a role, add them to the corresponding `KeycloakRealmGroup` in the `cozy` realm — either through the Keycloak UI or with a `KeycloakRealmUser` CR: + +```yaml +apiVersion: v1.edp.epam.com/v1 +kind: KeycloakRealmUser +metadata: + name: alice-acme + namespace: cozy-keycloak +spec: + realm: cozy + username: alice@acme.example + email: alice@acme.example + emailVerified: true + password: "…" + groups: + - tenant-acme-monitoring-admin +``` + +Removing a user from the group demotes them on the next login; deleting them from `cozy` revokes access outright. + +## Sign in + +Open `https://grafana.` and use the "Sign in with Keycloak" button under the login form. Grafana runs the Authorization Code + PKCE flow against `cozy`, receives a token whose `aud` matches this Monitoring instance's clientId, and creates or updates the local Grafana user on the first successful login with the role from `role_attribute_path`. + +The `admin_user` / `admin_password` field on the form stays wired to `grafana-admin-password` and continues to work. + +## Prerequisites and gotchas + +- **`emailVerified: true` on Keycloak users.** Phase 1 does not add a `claimValidationRules` entry — so `email_verified` is not chart-enforced. Set `emailVerified: true` on the `KeycloakRealmUser` (or complete the email-verify flow through the Keycloak UI) so the identity holding a given email is guaranteed authentic. The `cozy` realm's default `duplicateEmails: false` prevents a second account from claiming an already-registered address. CEL `claimValidationRules` to make this a hard gate is a follow-up hardening path. +- **BYO issuer with a self-signed CA.** In `CustomConfig` mode the `secretRef` path is the way to ship a CA bundle alongside the `[auth.generic_oauth]` block — you package `auth.ini` and any `ca-cert` files into the Secret and mount both under `/etc/grafana/oidc`. +- **`admin_user` stays a break-glass path.** Even under `mode: System` the login form and the `grafana-admin-password` Secret remain wired. Locking the form off is a follow-up hardening. + +## What's out of scope for this feature + +- **Per-tenant Keycloak realms.** Managed multi-tenant identity is a separate proposal, evaluated against Keycloak Organizations. Track it in the [community proposal](https://github.com/cozystack/community/pull/24). +- **Federating an external IdP into the platform `cozy` realm.** BYO-for-Cozystack-itself is a distinct problem — this feature is BYO-for-a-managed-service. +- **Full-logout through Keycloak's end-session endpoint.** Native `auth.generic_oauth` covers the OAuth part; `backend-logout-url` wiring is a follow-up. +- **CEL `claimValidationRules` for `email_verified`.** Explicit-gate hardening; not required for Phase 1 given the layered guarantees above. From 3aa9c29ec68e879094ccb7e8a3f338de96f1f155 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Sat, 4 Jul 2026 23:17:06 +0300 Subject: [PATCH 2/3] docs(monitoring): explicit oidc.grafanaAdmin flag + mode-toggle group loss note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two doc updates paired with the cozystack chart fix: - Server-level GrafanaAdmin promotion is now driven by an explicit spec.oidc.grafanaAdmin: true field (was: chart-side .Release.Name detection). Document the new field and where the platform bundle flips it on. - Mode toggle (System → CustomConfig/None) deletes the three chart-owned -{admin,editor,viewer} KeycloakRealmGroups and every user's membership goes with them. Not a bug, but a one- way UX cost on the toggle — document it under Prerequisites and gotchas so operators know before flipping. Signed-off-by: IvanHunters --- .../next/operations/services/monitoring/oidc-authentication.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content/en/docs/next/operations/services/monitoring/oidc-authentication.md b/content/en/docs/next/operations/services/monitoring/oidc-authentication.md index 34cbd77f..49139e03 100644 --- a/content/en/docs/next/operations/services/monitoring/oidc-authentication.md +++ b/content/en/docs/next/operations/services/monitoring/oidc-authentication.md @@ -43,7 +43,7 @@ Cozystack provisions: - A persistent Kubernetes Secret carrying the confidential `client-secret` (random on first install, preserved on upgrades). - The Grafana CR's `spec.config.auth.generic_oauth` section wired to the cozy realm issuer, per-instance audience scope, and a `role_attribute_path` that maps the three groups above to Grafana's `Admin` / `Editor` / `Viewer` roles. -The platform Grafana release (`monitoring-system` in the `cozy-monitoring` namespace) additionally gets `allow_assign_grafana_admin: true` so an `Admin`-group member is auto-promoted to server-level `GrafanaAdmin`. Tenant Grafana instances stay at org-level Admin. +Server-level `GrafanaAdmin` promotion is opt-in via `spec.oidc.grafanaAdmin: true` — the chart then sets `allow_assign_grafana_admin: true` on the rendered `[auth.generic_oauth]` block, so an `-admin` group member becomes a `GrafanaAdmin` on first login. The platform bundle flips this on for the platform Grafana release (`monitoring-system` in `cozy-monitoring`); tenant Grafana releases inherit the chart default `false` and stay at org-level Admin. ### Prerequisite @@ -123,6 +123,7 @@ The `admin_user` / `admin_password` field on the form stays wired to `grafana-ad - **`emailVerified: true` on Keycloak users.** Phase 1 does not add a `claimValidationRules` entry — so `email_verified` is not chart-enforced. Set `emailVerified: true` on the `KeycloakRealmUser` (or complete the email-verify flow through the Keycloak UI) so the identity holding a given email is guaranteed authentic. The `cozy` realm's default `duplicateEmails: false` prevents a second account from claiming an already-registered address. CEL `claimValidationRules` to make this a hard gate is a follow-up hardening path. - **BYO issuer with a self-signed CA.** In `CustomConfig` mode the `secretRef` path is the way to ship a CA bundle alongside the `[auth.generic_oauth]` block — you package `auth.ini` and any `ca-cert` files into the Secret and mount both under `/etc/grafana/oidc`. - **`admin_user` stays a break-glass path.** Even under `mode: System` the login form and the `grafana-admin-password` Secret remain wired. Locking the form off is a follow-up hardening. +- **Mode toggle wipes `KeycloakRealmGroup` membership.** The three `-{admin,editor,viewer}` groups are chart-owned. Flipping `spec.oidc.mode` from `System` to `CustomConfig` or `None` deletes them on the next `helm upgrade` — and every user's membership goes with them. Flipping back recreates the groups empty; the platform operator has to re-populate them by hand. Treat mode toggle like an admin-Secret rotation. ## What's out of scope for this feature From 710a840a881fecdc5969806b7baec04affccc3c7 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Tue, 14 Jul 2026 00:47:53 +0300 Subject: [PATCH 3/3] docs(monitoring): rewrite Grafana OIDC guide to match Phase-1 users-Job architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The website doc was written against the initial PR#3176 shape (three chart-owned KeycloakRealmGroups + role_attribute_path mapping groups to Admin/Editor/Viewer). Later commits on the code PR dropped that architecture entirely — chart-owned groups gone (commit 3b36966e2), role_attribute_path replaced with skip_org_role_sync + app-side users-map reconciled by a Job (commit sequence 2b3751edc / 7e2ac3cb8 / 4e8c5f4ed / e47308961 / b370998fc / efc27b33c). The website guide had drifted six revisions behind the code. Rewrite covers the actual current surface: - spec.oidc.users[] with Grafana org-role (Admin/Editor/Viewer) is the assignment mechanism; users-Job reconciles it on every helm action. - Login authorization is separate: allowed_groups gate on the tenant-scoped -{view,use,admin,super-admin} groups from the platform-managed cozy realm. - groups_attribute_path=groups is REQUIRED on Grafana v11.x+ alongside allowed_groups (matches the code fix committed on the cozystack side). - CustomConfig inline vs secretRef with the users[] compatibility contract (secretRef requires users: []). - Mode-toggle gotcha now describes the users-Job's ephemeral-account cleanup, not the deleted KeycloakRealmGroups. Also drops the stale role_attribute_path example that used a grafana-admins group name that this chart never provisioned. Signed-off-by: IvanHunters --- .../monitoring/oidc-authentication.md | 73 +++++++++++++------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/content/en/docs/next/operations/services/monitoring/oidc-authentication.md b/content/en/docs/next/operations/services/monitoring/oidc-authentication.md index 49139e03..5f43acd2 100644 --- a/content/en/docs/next/operations/services/monitoring/oidc-authentication.md +++ b/content/en/docs/next/operations/services/monitoring/oidc-authentication.md @@ -1,16 +1,16 @@ --- title: "OIDC authentication for Grafana" linkTitle: "OIDC authentication" -description: "Give tenant users per-identity Grafana access with per-instance audience isolation." +description: "Give tenant users per-identity Grafana access with per-instance audience isolation and tenant-membership gating." weight: 5 --- -Cozystack Grafana instances can authenticate users through OIDC instead of the shared `admin_user` / `admin_password` Secret. Each user then has their own identity, per-user audit, and a role that can be revoked by removing them from a Keycloak group. +Cozystack Grafana instances can authenticate users through OIDC instead of the shared `admin_user` / `admin_password` Secret. Each user then has their own identity, per-user audit, and a role that can be revoked by removing them from `spec.oidc.users` on the `Monitoring` CR. -The identity model is deliberately **per-instance** rather than per-tenant: each Monitoring release (per-tenant `monitoring`, plus the platform's `monitoring-system`) gets its own OIDC audience, and a token minted for instance A is rejected by instance B's Grafana. That gives cross-tenant isolation without provisioning a Keycloak realm per tenant. The full rationale (why per-cluster audience and not per-tenant realm; how it relates to Keycloak Organizations; what BYO-OIDC looks like) is in the [design proposal](https://github.com/cozystack/community/pull/24). The tenant kube-apiserver's Phase 1 ([cozystack/cozystack#3044](https://github.com/cozystack/cozystack/pull/3044)) uses the same shape; this Grafana integration is the Phase-1 follow-up called out in that PR's body. +The identity model is deliberately **per-instance** rather than per-tenant: each Monitoring release (per-tenant `monitoring`, plus the platform's `monitoring-system`) gets its own OIDC audience, and a token minted for instance A is rejected by instance B's Grafana. Cross-tenant sign-in is additionally blocked by a `allowed_groups` gate on the release's namespace-scoped `-{view,use,admin,super-admin}` groups (chart-owned in the tenant chart; the platform-managed `groups` scope in the `cozy` realm makes them visible on every token). The full rationale is in the [design proposal](https://github.com/cozystack/community/pull/24). The tenant kube-apiserver's Phase 1 ([cozystack/cozystack#3044](https://github.com/cozystack/cozystack/pull/3044)) uses the same shape; this Grafana integration is the Phase-1 follow-up called out in that PR's body. {{% alert color="info" %}} -The `grafana-admin-password` Secret in the tenant namespace stays available as a break-glass path regardless of whether OIDC is enabled. `disable_login_form` is not flipped by the selector. +The `grafana-admin-password` Secret in the release namespace stays available as a break-glass path regardless of whether OIDC is enabled. `disable_login_form` is not flipped by the selector. {{% /alert %}} ## Modes @@ -32,18 +32,42 @@ metadata: spec: oidc: mode: System + users: + - email: alice@acme.example + role: Admin + - email: bob@acme.example + role: Editor + - email: carol@acme.example + role: Viewer # ... ``` Cozystack provisions: -- A per-instance `KeycloakClient` in the `cozy` realm with `clientId` set to `-` (for the CR above: `tenant-acme-monitoring`). `public: false`, `directAccess: false`, `secret` sourced from a chart-owned Kubernetes Secret. `redirectUris` locked to `https://grafana./login/generic_oauth`. -- A per-instance `KeycloakClientScope` whose audience mapper pins the token's `aud` claim to that same `clientId` — the isolation primitive. -- Three `KeycloakRealmGroup` objects — `--admin`, `-editor`, `-viewer`. The chart owns the groups; membership is managed out-of-band by a Keycloak operator. -- A persistent Kubernetes Secret carrying the confidential `client-secret` (random on first install, preserved on upgrades). -- The Grafana CR's `spec.config.auth.generic_oauth` section wired to the cozy realm issuer, per-instance audience scope, and a `role_attribute_path` that maps the three groups above to Grafana's `Admin` / `Editor` / `Viewer` roles. +- A per-instance **`KeycloakClient`** in the `cozy` realm with `clientId` set to `-` (for the CR above: `tenant-acme-monitoring-system`). Confidential (`clientAuthenticatorType: client-secret`), `secret` sourced from a chart-owned Kubernetes Secret. `redirectUris` locked to `https://grafana./login/generic_oauth`. +- A per-instance **`KeycloakClientScope`** whose audience mapper pins the token's `aud` claim to that same `clientId` — the isolation primitive. +- A persistent Kubernetes **Secret** carrying the confidential `client-secret` (random on first install, preserved on upgrades). +- A chart-owned **users-reconcile Job** (`-oidc-users`) that syncs `spec.oidc.users[]` into the Grafana instance on every reconcile: creates missing Grafana accounts, patches roles, and prunes stale org members. Runs as a post-install / post-upgrade hook when `users[]` is non-empty; omitted otherwise (BYO-IdP-friendly). +- The Grafana CR's **`spec.config.auth.generic_oauth`** section wired to the cozy realm issuer, per-instance audience scope, and the tenant-membership gate: -Server-level `GrafanaAdmin` promotion is opt-in via `spec.oidc.grafanaAdmin: true` — the chart then sets `allow_assign_grafana_admin: true` on the rendered `[auth.generic_oauth]` block, so an `-admin` group member becomes a `GrafanaAdmin` on first login. The platform bundle flips this on for the platform Grafana release (`monitoring-system` in `cozy-monitoring`); tenant Grafana releases inherit the chart default `false` and stay at org-level Admin. + ``` + allowed_groups = -view -use -admin -super-admin + groups_attribute_path = groups + ``` + + `groups_attribute_path` is REQUIRED alongside `allowed_groups` on Grafana v11.x+: without it Grafana leaves the extracted `user.Groups` slice empty regardless of what the userinfo endpoint returns, and the gate rejects every login with "user not a member of one of the required groups" even when the token carries the correct claim. The JMESPath expression `groups` reads the top-level `groups` array from userinfo — the shape Keycloak's `oidc-group-membership-mapper` emits. + + When `spec.oidc.users[]` is non-empty the chart additionally forces: + + ``` + skip_org_role_sync = true + oauth_allow_insecure_email_lookup = true + allow_sign_up = false + ``` + + `skip_org_role_sync=true` keeps a login from overwriting the users-Job's role assignments; `oauth_allow_insecure_email_lookup=true` lets Grafana attach the OIDC identity to the pre-provisioned local account by email; `allow_sign_up=false` is the isolation lever — without it, Grafana's default `allow_sign_up=true` combined with `skip_org_role_sync=true` would mint a Viewer account for every `cozy`-realm identity that hits the login flow. + +Server-level `GrafanaAdmin` promotion is opt-in via `spec.oidc.grafanaAdmin: true` — the chart then sets `allow_assign_grafana_admin: true` on the rendered `[auth.generic_oauth]` block. The platform bundle flips this on for the platform Grafana release (`monitoring-system` in `cozy-monitoring`); tenant Grafana releases inherit the chart default `false` and stay at org-level Admin. ### Prerequisite @@ -65,7 +89,9 @@ spec: token_url: https://idp.acme.example/protocol/openid-connect/token api_url: https://idp.acme.example/protocol/openid-connect/userinfo scopes: openid email profile groups - role_attribute_path: "contains(groups[*], 'grafana-admins') && 'Admin' || 'Viewer'" + users: + - email: alice@acme.example + role: Admin ``` …or via a pre-existing Secret in the tenant namespace holding a ready-made `[auth.generic_oauth]` ini fragment in the `auth.ini` key: @@ -81,18 +107,17 @@ spec: Setting both `config` and `secretRef.name` (or neither) fails the render. In `CustomConfig` mode no Keycloak objects are provisioned in `cozy`; the Grafana instance trusts the operator-supplied issuer directly. +The `secretRef` path is authoritative — the chart does not overlay any keys onto the operator-supplied ini fragment. If you use `secretRef`, `spec.oidc.users[]` MUST be empty (the chart fails the render otherwise) because the users-Job cannot reason about a config it cannot see. + +The `inline config` path merges the operator's map on top of the chart-forced users-map contract (`skip_org_role_sync=true`, `oauth_allow_insecure_email_lookup=true`, `allow_sign_up=false`) when `users[]` is non-empty. If your BYO IdP does not emit `groups` in the shape Grafana expects, add `groups_attribute_path` to your inline map to point at the right JMESPath. + ## Assigning roles -Grafana has three org-level roles — `Admin`, `Editor`, `Viewer` — and the chart drives them via `role_attribute_path`: +Roles are driven by **`spec.oidc.users[]`**, not by group membership: each entry has an `email` (matched against the OIDC `email` claim on login) and a `role` (Grafana org-level: `Admin`, `Editor`, or `Viewer`). The users-Job creates or updates the local Grafana account on every reconcile and prunes stale members. Removing a user from the list demotes them (users-Job deletes the account) on the next chart apply; adding a user re-provisions them. -```text -contains(groups[*], '--admin') && 'Admin' || -contains(groups[*], '--editor') && 'Editor' || -contains(groups[*], '--viewer') && 'Viewer' || -'Viewer' -``` +Login authorization is separate: the token owner MUST be a member of one of the release's tenant-scoped `cozy`-realm groups (`-view`, `-use`, `-admin`, `-super-admin`) for `allowed_groups` to accept the login. That gate is unconditional in `System` mode, independent of `users[]`. -Authenticated users with none of the three groups default to `Viewer`. To give a user a role, add them to the corresponding `KeycloakRealmGroup` in the `cozy` realm — either through the Keycloak UI or with a `KeycloakRealmUser` CR: +Add a user to the appropriate tenant-scoped group in `cozy` — via the Keycloak UI or with a `KeycloakRealmUser` CR: ```yaml apiVersion: v1.edp.epam.com/v1 @@ -107,23 +132,22 @@ spec: emailVerified: true password: "…" groups: - - tenant-acme-monitoring-admin + - tenant-acme-admin ``` -Removing a user from the group demotes them on the next login; deleting them from `cozy` revokes access outright. - ## Sign in -Open `https://grafana.` and use the "Sign in with Keycloak" button under the login form. Grafana runs the Authorization Code + PKCE flow against `cozy`, receives a token whose `aud` matches this Monitoring instance's clientId, and creates or updates the local Grafana user on the first successful login with the role from `role_attribute_path`. +Open `https://grafana.` and press "Sign in with Keycloak" under the login form. Grafana runs the OAuth Authorization Code flow against `cozy`, receives a token whose `aud` matches this Monitoring instance's clientId, checks the `groups` claim against `allowed_groups`, then looks up the pre-provisioned Grafana account by email and grants the users-Job-assigned role. The `admin_user` / `admin_password` field on the form stays wired to `grafana-admin-password` and continues to work. ## Prerequisites and gotchas - **`emailVerified: true` on Keycloak users.** Phase 1 does not add a `claimValidationRules` entry — so `email_verified` is not chart-enforced. Set `emailVerified: true` on the `KeycloakRealmUser` (or complete the email-verify flow through the Keycloak UI) so the identity holding a given email is guaranteed authentic. The `cozy` realm's default `duplicateEmails: false` prevents a second account from claiming an already-registered address. CEL `claimValidationRules` to make this a hard gate is a follow-up hardening path. +- **`groups_attribute_path` is not optional on Grafana v11.x+.** The chart wires it automatically for `System` mode; in `CustomConfig` inline the operator must add it explicitly (`groups_attribute_path: groups`) if their IdP emits a top-level `groups` array. Otherwise `allowed_groups` becomes a silent no-op and every login fails. - **BYO issuer with a self-signed CA.** In `CustomConfig` mode the `secretRef` path is the way to ship a CA bundle alongside the `[auth.generic_oauth]` block — you package `auth.ini` and any `ca-cert` files into the Secret and mount both under `/etc/grafana/oidc`. - **`admin_user` stays a break-glass path.** Even under `mode: System` the login form and the `grafana-admin-password` Secret remain wired. Locking the form off is a follow-up hardening. -- **Mode toggle wipes `KeycloakRealmGroup` membership.** The three `-{admin,editor,viewer}` groups are chart-owned. Flipping `spec.oidc.mode` from `System` to `CustomConfig` or `None` deletes them on the next `helm upgrade` — and every user's membership goes with them. Flipping back recreates the groups empty; the platform operator has to re-populate them by hand. Treat mode toggle like an admin-Secret rotation. +- **Mode toggle drops the users-Job's local accounts.** Flipping `spec.oidc.mode` from `System` to `None` prunes every user the users-Job provisioned (the accounts are ephemeral shadows of `spec.oidc.users[]`). Any dashboards those users starred / owned move to their original creators; the local passwords / API keys go away. Flipping back re-provisions them empty. Treat mode toggle like an admin-Secret rotation. ## What's out of scope for this feature @@ -131,3 +155,4 @@ The `admin_user` / `admin_password` field on the form stays wired to `grafana-ad - **Federating an external IdP into the platform `cozy` realm.** BYO-for-Cozystack-itself is a distinct problem — this feature is BYO-for-a-managed-service. - **Full-logout through Keycloak's end-session endpoint.** Native `auth.generic_oauth` covers the OAuth part; `backend-logout-url` wiring is a follow-up. - **CEL `claimValidationRules` for `email_verified`.** Explicit-gate hardening; not required for Phase 1 given the layered guarantees above. +- **Role granularity beyond Admin/Editor/Viewer.** Grafana org roles are the assignment surface; team memberships / dashboard-level permissions stay out-of-band.