-
Notifications
You must be signed in to change notification settings - Fork 31
fix(monitoring): publish the Grafana OIDC guide in v1.6 #647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| --- | ||
| title: "OIDC authentication for Grafana" | ||
| linkTitle: "OIDC authentication" | ||
| 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 `spec.oidc.users` on the `Monitoring` CR. | ||
|
|
||
| The identity model is deliberately **per-instance** rather than per-tenant: every Monitoring instance (one inner `monitoring-system` release per namespace — each tenant's own, plus the platform's in `tenant-root`) 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 `<ns>-{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 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 | ||
|
|
||
| `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 | ||
| users: | ||
| - email: alice@acme.example | ||
| role: Admin | ||
| - email: bob@acme.example | ||
| role: Editor | ||
| - email: carol@acme.example | ||
| role: Viewer | ||
| # ... | ||
| ``` | ||
|
|
||
| Cozystack provisions the following. Every derived identifier below uses `<release>`, the name of the inner HelmRelease that actually carries the OIDC templates. A `Monitoring` CR's release name is forced to `monitoring` (`check-release-name.yaml` rejects any other name), and its inner release is always `<release-name>-system`, so `<release>` is literally `monitoring-system` in every namespace — `tenant-acme-monitoring-system` for the CR above. Instances are told apart by namespace, not by release name: | ||
|
|
||
| - A per-instance **`KeycloakClient`** in the `cozy` realm with `clientId` set to `<namespace>-<release>` (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.<host>/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** (`<release>-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). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 8 \
'users-reconcile|allow_sign_up|spec\.oidc\.users|prune|mode' .Repository: cozystack/website Length of output: 50374 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
file='content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md'
printf '%s\n' '--- target document ---'
sed -n '40,125p' "$file"
printf '%s\n' '--- related files and exact references ---'
rg -n -C 3 --glob '!**/public/**' --glob '!**/static/**' \
'users-reconcile|oidc-users|allow_sign_up|spec\.oidc\.users|prune stale|revok' \
content charts deploy config . 2>/dev/null | head -n 500 || true
printf '%s\n' '--- versioned copies ---'
fd -t f 'oidc-authentication\.md$' contentRepository: cozystack/website Length of output: 50375 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
target = Path("content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md")
text = target.read_text()
for term in ("users-reconcile", "oidc-users", "allow_sign_up", "spec.oidc.users", "prune", "revok"):
print(f"{term}: {len(re.findall(re.escape(term), text, flags=re.I))} matches in {target}")
PYRepository: cozystack/website Length of output: 758 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
file='content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md'
printf '%s\n' '--- relevant v1.6 statements ---'
sed -n '56,70p;111,118p;145,156p' "$file"
printf '%s\n' '--- repository implementation candidates ---'
git ls-files | rg -i '(^|/)(charts?|templates?|monitoring|grafana|oidc|helm|cozystack)(/|$)|users-reconcile|oidc-users'Repository: cozystack/website Length of output: 15373 🌐 Web query:
💡 Result: In Grafana's configuration for Generic OAuth, the Citations:
🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
text = Path("content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md").read_text()
required = [
"When `spec.oidc.users[]` is non-empty",
"allow_sign_up = false",
"The users-Job renders only when `spec.oidc.mode` is not `None` *and* `spec.oidc.users[]` is non-empty",
"Removing a user from the list revokes their access on the next chart apply",
]
missing = [item for item in required if item not in text]
if missing:
raise SystemExit(f"missing expected documentation statements: {missing}")
nonempty_start = text.index("When `spec.oidc.users[]` is non-empty")
nonempty_end = text.index("### Prerequisite", nonempty_start)
nonempty_section = text[nonempty_start:nonempty_end]
mode_start = text.index("- **Mode toggle is non-destructive.**")
mode_end = text.index("\n", text.index("survive untouched", mode_start)) + 1
mode_section = text[mode_start:mode_end]
assert "allow_sign_up = false" in nonempty_section
assert "users-Job renders only when" in mode_section
assert "spec.oidc.users[]` is non-empty" in mode_section
assert "runs no reconcile and no prune pass" in mode_section
assert "allow_sign_up" not in mode_section
print("Documented empty-list transition: no users-Job/prune pass and no documented forced allow_sign_up=false setting.")
PYRepository: cozystack/website Length of output: 267 Handle the transition to an empty When 🤖 Prompt for AI AgentsSource: MCP tools |
||
| - An **`auth.generic_oauth`** entry under the Grafana CR's **`spec.config`** (a single ini-section key literally named `auth.generic_oauth`, so it is addressed as `spec.config["auth.generic_oauth"]`, not a two-level `auth` / `generic_oauth` nesting), wired to the cozy realm issuer, per-instance audience scope, and the tenant-membership gate: | ||
|
|
||
| ```ini | ||
| allowed_groups = <namespace>-view <namespace>-use <namespace>-admin <namespace>-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: | ||
|
|
||
| ```ini | ||
| skip_org_role_sync = true | ||
| oauth_allow_insecure_email_lookup = true | ||
| allow_sign_up = false | ||
|
Comment on lines
+62
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 8 \
'oauth_allow_insecure_email_lookup|auth\.generic_oauth|spec\.config' .Repository: cozystack/website Length of output: 50375 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- repository files near monitoring documentation ---'
git ls-files | rg '(^|/)(charts?|packages|templates|values|.*monitoring.*|.*grafana.*)' | head -200
printf '%s\n' '--- exact configuration references ---'
rg -n -C 5 \
'oauth_allow_insecure_email_lookup|skip_org_role_sync|allow_sign_up|auth\.generic_oauth|spec\.config' \
content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md \
content/en/docs/v1.6/operations/services/monitoring/parameters.mdRepository: cozystack/website Length of output: 27436 🌐 Web query:
💡 Result: The oauth_allow_insecure_email_lookup configuration option in Grafana determines whether the system uses a user's email address as a unique identifier for authentication, rather than the unique ID provided by the Identity Provider (IdP) [1][2]. This option is disabled by default [1][3]. Grafana strongly recommends against enabling this feature because it can lower the security of your Grafana instance and may expose it to potential account takeovers or authentication bypasses, as it was originally changed to address security vulnerabilities such as CVE-2023-3128 [1][4]. Key details regarding this configuration: Configuration To enable it, you must set the following in your Grafana configuration file: [auth] oauth_allow_insecure_email_lookup = true Alternatively, it can be enabled via: - API: Using the /api/admin/settings endpoint [1][5]. - UI: Navigating to Administration > Authentication > Auth settings [1][2]. Security Warning If you choose to enable this option, ensure that settings such as Allowed organization, Allowed groups, and Allowed domains are strictly configured to prevent unauthorized access [1][3]. Do not enable this if you are using multi-tenant applications (such as Azure AD in a multi-tenant configuration) [4]. Context This feature was introduced as an escape hatch in Grafana v10.0 following breaking changes that restricted OAuth integrations from relying solely on email lookups to improve security [4]. Since Grafana v10.4, this setting has also been configurable via the API for Grafana Enterprise and Grafana Cloud [1][5]. Citations:
Render Grafana reads this option only from the global 🤖 Prompt for AI AgentsSource: MCP tools |
||
| ``` | ||
|
|
||
| `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. | ||
|
Comment on lines
+62
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file='content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md'
printf '%s\n' '--- target file ---'
cat -n "$file"
printf '%s\n' '--- related identifiers and files ---'
rg -n --hidden -g '!node_modules' -g '!dist' \
'oauth_allow_insecure_email_lookup|skip_org_role_sync|allow_sign_up|CustomConfig|users:|users\[\]|audience|email' \
content | head -n 240
printf '%s\n' '--- matching versioned documents ---'
find content/en/docs -path '*/operations/services/monitoring/oidc-authentication.md' -printRepository: cozystack/website Length of output: 50375 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository files related to monitoring OIDC ---'
git ls-files | rg -i 'monitoring|grafana|oidc|keycloak' | head -n 300
printf '%s\n' '--- implementation references ---'
rg -n --hidden -g '!content/**' -g '!node_modules' -g '!dist' \
'oauth_allow_insecure_email_lookup|skip_org_role_sync|allow_sign_up|oidc\.users|users-reconcile|users-reconcile|email_verified|emailVerified|auth\.generic_oauth|CustomConfig' . \
| head -n 300Repository: cozystack/website Length of output: 10615 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository files related to monitoring OIDC ---'
git ls-files | rg -i 'monitoring|grafana|oidc|keycloak' | head -n 300
printf '%s\n' '--- implementation references ---'
rg -n --hidden -g '!content/**' -g '!node_modules' -g '!dist' \
'oauth_allow_insecure_email_lookup|skip_org_role_sync|allow_sign_up|oidc\.users|users-reconcile|email_verified|emailVerified|auth\.generic_oauth|CustomConfig' . \
| head -n 300Repository: cozystack/website Length of output: 10615 🌐 Web query:
💡 Result: The configuration setting oauth_allow_insecure_email_lookup is an "escape hatch" in Grafana designed to restore legacy behavior for user authentication, specifically allowing Grafana to identify users by their email address rather than relying exclusively on a unique ID provided by an identity provider (IdP) [1][2][3]. Security Risks and Context This setting is disabled by default because it introduces significant security risks, most notably account takeover and authentication bypass [1][2]. The primary vulnerabilities associated with relying on email lookups include: 1. Account Takeover and Authentication Bypass (CVE-2023-3128): Grafana previously validated accounts based on email claims [1][3]. In many IdP environments, such as multi-tenant Azure Active Directory (Azure AD) applications, email fields are not guaranteed to be unique and can sometimes be modified by users [1][4][5]. If an attacker can manipulate or use a non-unique email address that matches an existing Grafana user, they may be able to authenticate as that user, leading to a complete account takeover [1][5]. 2. Legacy Vulnerability (CVE-2022-31107): Prior to this, similar account takeover issues were identified where an attacker could link an external OAuth identity to an existing internal Grafana account by knowing the target user's username if the email or external ID was not already associated [6]. Official Recommendation Grafana Labs strongly recommends against enabling oauth_allow_insecure_email_lookup [2][7]. They advise that users ensure unique identification across identity providers by relying on the unique ID (e.g., the sub claim in OIDC) provided by the IdP rather than an email address [1][2]. If an organization absolutely must enable this setting, Grafana emphasizes that it should only be used when: - The identity provider is a single-tenant setup [2][7]. - The IdP provides unique, validated, and non-editable emails [2][7]. - Strict configuration of Allowed organization, Allowed groups, and Allowed domains is enforced to mitigate the increased risk of unauthorized access [2][8]. In summary, enabling oauth_allow_insecure_email_lookup reverts Grafana to a less secure authentication model that relies on mutable or non-unique email claims, thereby re-opening the application to potential account takeover vulnerabilities [1][3]. Citations:
🌐 Web query:
💡 Result: In OpenID Connect (OIDC), the email address is not a stable identifier and must not be used as a primary key for identifying users [1][2][3][4]. The specification explicitly distinguishes between stable identifiers and mutable profile attributes: 1. The sub (Subject) Claim: This is the only claim that a Relying Party (RP) can rely on as a stable, unique identifier for an End-User at a specific Issuer [3]. The OpenID Connect Core specification mandates that the sub value must be locally unique and never reassigned within the Issuer for a particular End-User [3]. 2. The Email Claim: The email address is considered a mutable profile attribute, not a permanent identifier [2][3]. It can change over time, be recycled, or be reassigned by the Identity Provider (IdP) to different users [3][4]. Consequently, the specification states that the email claim must not be relied upon to be unique by the RP [5][3][4]. 3. Recommended Identifier Strategy: To uniquely and safely identify a user across federated services, RPs must use the combination of the issuer (iss) and subject (sub) claims [1][6][2][7]. The (iss, sub) pair provides a composite identifier that remains stable even if a user's email address, username, or other profile information changes [2][7]. 4. Email_Verified Claim: The email_verified claim is a boolean value indicating whether the IdP has verified that the email address belongs to the user [8][5][3]. It does not confer stability or uniqueness to the email address itself; it only indicates the verification status of that address at the time the ID Token was issued [3]. Using email as a primary key is considered a significant security anti-pattern (often called the False Identifier anti-pattern) that can lead to account takeover vulnerabilities and data merging issues if an email address is re-registered by a new user [6][2][4]. Citations:
Require a trusted email identity for When 🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
| ### 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: | ||
| enabled: "true" | ||
| 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 | ||
|
Comment on lines
+82
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Warn against storing The example places the OAuth client credential directly in 🤖 Prompt for AI Agents
Comment on lines
+89
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file='content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md'
printf '%s\n' '--- target file ---'
sed -n '1,180p' "$file"
printf '%s\n' '--- related OIDC scope references ---'
rg -n -i --glob '*.md' --glob '*.yaml' --glob '*.yml' \
'groups|openid|userinfo|scope' content/en/docs/v1.6 content/en/docs/next 2>/dev/null \
| head -n 250Repository: cozystack/website Length of output: 50375 🌐 Web query:
💡 Result: In OpenID Connect (OIDC) Core 1.0, scopes are used by clients to request that specific sets of information (Claims) be returned [1][2]. Standard OIDC Scope Values: The OIDC specification defines several standard scope values that map to specific sets of Claims: - openid: REQUIRED. This scope value must be included in the Authorization Request to indicate that the request is an OpenID Connect request [1][3]. - profile: Requests access to the End-User's default profile Claims (e.g., name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at) [1][4]. - email: Requests access to the email and email_verified Claims [1][4]. - address: Requests access to the address Claim [1]. - phone: Requests access to the phone_number and phone_number_verified Claims [1]. Groups and Provider-Specific Claims: The OIDC Core 1.0 specification does not define a standard groups scope or a standard groups claim [1][4]. Because there is no universal standard for representing groups, this functionality is handled as a provider-specific implementation [5]. - Implementation: When a provider (such as Okta or others) supports a groups claim, they typically implement it by defining their own custom scope (e.g., groups) or by including the claim automatically when other scopes are requested [5]. - Discovery: Providers that support custom claims or non-standard scopes often advertise these through the OpenID Connect Discovery 1.0 mechanism [6]. By accessing the provider's discovery endpoint (typically located at.well-known/openid-configuration), developers can see which claims and scopes the specific provider supports [6]. In summary, while the core specification provides a framework for standard scopes, any functionality not explicitly defined—such as specific groups handling—is left to the discretion of the OpenID Provider [1][4]. Developers should consult the provider's specific documentation or their discovery document to identify available custom scopes and claims [6]. Citations:
Make
🤖 Prompt for AI AgentsSource: MCP tools |
||
| 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: | ||
|
|
||
| ```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. | ||
|
|
||
| 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. | ||
|
|
||
| On the `inline config` path, when `users[]` is non-empty the chart-forced users-map contract (`skip_org_role_sync=true`, `oauth_allow_insecure_email_lookup=true`, `allow_sign_up=false`) is merged *over* your map and wins every conflict: the chart renders `merge $chartForced $yourConfig`, and Helm's `merge` keeps the left-hand (chart) values and only fills in keys your map is missing — so you cannot override those three. Unlike `System` mode, `CustomConfig` does not set `enabled` for you: your `config` MUST include `enabled: "true"` or Grafana leaves `auth.generic_oauth` disabled and no sign-in button appears. 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 | ||
|
|
||
| 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 revokes their access on the next chart apply — the users-Job removes them from the Grafana org (`DELETE /api/orgs/1/users/<id>`), an org-membership change rather than deletion of the underlying Grafana account; adding a user re-provisions them. | ||
|
|
||
| Login authorization is separate: the token owner MUST be a member of one of the release's tenant-scoped `cozy`-realm groups (`<namespace>-view`, `<namespace>-use`, `<namespace>-admin`, `<namespace>-super-admin`) for `allowed_groups` to accept the login. That gate is unconditional in `System` mode, independent of `users[]`. | ||
|
|
||
| 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 | ||
| kind: KeycloakRealmUser | ||
| metadata: | ||
| name: alice-acme | ||
| namespace: cozy-keycloak | ||
| spec: | ||
| realmRef: | ||
| name: keycloakrealm-cozy | ||
| kind: ClusterKeycloakRealm | ||
| username: alice@acme.example | ||
| email: alice@acme.example | ||
| emailVerified: true | ||
| passwordSecret: | ||
| name: alice-acme-password | ||
| key: password | ||
| groups: | ||
| - tenant-acme-admin | ||
| ``` | ||
|
|
||
| The `cozy` realm is a cluster-scoped `ClusterKeycloakRealm` named `keycloakrealm-cozy`, so `spec.realmRef` (not `spec.realm`) is required, with `kind: ClusterKeycloakRealm`. `spec.passwordSecret` points at a Secret in the same namespace holding the initial password (`kubectl create secret generic alice-acme-password --namespace cozy-keycloak --from-literal=password=...`); the CRD also accepts an inline `spec.password`, but it is deprecated in favour of `passwordSecret`. | ||
|
|
||
| ## Sign in | ||
|
|
||
| Open `https://grafana.<host>` 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.** `email_verified` is not enforced by Grafana: its `auth.generic_oauth` provider has no claim-validation option (claim handling is JMESPath only, and `role_attribute_path` is unused here because the users-Job drives roles). Enforce it on the Keycloak side instead: set `emailVerified: true` on the `KeycloakRealmUser` (or add the `VERIFY_EMAIL` required action / complete the email-verify flow in the Keycloak UI) so the identity holding a given email is guaranteed authentic. The `cozy` realm's default `duplicateEmails: false` additionally prevents a second account from claiming an already-registered address. | ||
| - **`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. | ||
|
Comment on lines
+151
to
+152
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file='content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md'
printf '%s\n' '--- target section ---'
sed -n '120,165p' "$file"
printf '%s\n' '--- references to group settings ---'
rg -n -C 3 'groups_attribute_path|allowed_groups|CustomConfig|System' content/en/docs/v1.6 content/en/docs/next 2>/dev/null || trueRepository: cozystack/website Length of output: 50376 🌐 Web query:
💡 Result: When using Generic OAuth in Grafana, configuring allowed_groups creates a strict requirement that mandates the user to be a member of at least one group defined in that list [1][2]. If Grafana fails to retrieve any groups for the user—or if the groups retrieved do not match any of the values in the allowed_groups list—the login will be rejected [1][3]. Key points regarding this behavior include: 1. Mandatory Dependency: If you configure allowed_groups, you are required to also configure groups_attribute_path [1][2]. 2. Resolution Order: Grafana attempts to evaluate the groups_attribute_path JMESPath expression sequentially against three sources: the OAuth2 ID token, the UserInfo endpoint response, and finally the OAuth2 access token [1][4]. It stops once it successfully resolves a group array [1]. 3. Rejection Cause: If the expression results in an empty list, null, or a set of groups that does not overlap with allowed_groups, Grafana denies the user access [1][5]. 4. Troubleshooting: - Ensure your OAuth provider is actually returning the groups claim in the ID token or UserInfo response [6]. - Verify that your JMESPath expression in groups_attribute_path correctly targets the path where groups are located in the JSON response [7][4]. - Inspect Grafana logs (at debug level) to see the raw JSON response received from the provider, which will help verify if the group data is present or empty [8][9]. If a user is being rejected unexpectedly, it is usually because the groups are either missing from the token/UserInfo response or the JMESPath expression is incorrectly configured and failing to extract the group array [1][8]. Citations:
Correct the Without 🤖 Prompt for AI AgentsSource: MCP tools |
||
| - **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`. Placing the file is not enough on its own: point `tls_client_ca` at it from inside your `auth.ini` so Grafana actually trusts the issuer's certificate. | ||
| - **`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 is non-destructive.** The users-Job renders only when `spec.oidc.mode` is not `None` *and* `spec.oidc.users[]` is non-empty. Flipping `spec.oidc.mode` from `System` to `None` therefore runs no reconcile and no prune pass: the Grafana accounts the Job provisioned survive untouched, and OIDC login is simply no longer offered on the form. Flipping back to `System` resumes reconciliation of `spec.oidc.users[]` against those existing accounts. | ||
|
|
||
| ## 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. | ||
| - **Server-level `GrafanaAdmin` promotion.** All Grafana instances — platform and tenant — cap at org-level `Admin`; `allow_assign_grafana_admin` is not wired and the `Monitoring` CR exposes no field to opt in. | ||
| - **Role granularity beyond Admin/Editor/Viewer.** Grafana org roles are the assignment surface; team memberships / dashboard-level permissions stay out-of-band. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Clarify group ownership for inherited Monitoring instances.
The tenant guide states that a child tenant can use its parent tenant's Monitoring instance. In that case,
<namespace>-view,<namespace>-use,<namespace>-admin, and<namespace>-super-adminrefer to the namespace that owns the Monitoring release, not necessarily the child tenant. Add this inheritance case to prevent operators from creating groups that the inherited Monitoring instance does not check.Also applies to: 117-117
🤖 Prompt for AI Agents