Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Copy link
Copy Markdown
Contributor

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-admin refer 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
at line 10, The Monitoring documentation must clarify inherited-instance group
ownership. Update the section describing the namespace-scoped
<ns>-{view,use,admin,super-admin} groups to state that <ns> is the namespace
owning the Monitoring release; when a child tenant inherits its parent’s
instance, operators must use the parent namespace groups rather than creating
child-tenant groups.


{{% 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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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$' content

Repository: 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}")
PY

Repository: 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:

Grafana official documentation generic OAuth allow_sign_up default oauth_allow_insecure_email_lookup

💡 Result:

In Grafana's configuration for Generic OAuth, the allow_sign_up and oauth_allow_insecure_email_lookup settings serve distinct purposes as follows: allow_sign_up This setting controls whether new Grafana users are automatically created when they authenticate via Generic OAuth [1][2]. Default value: true [1][2]. Behavior: When set to true, Grafana creates a new user account upon a successful login if one does not already exist [1][2]. If set to false, only users who already have a pre-existing account in Grafana can log in using the OAuth provider [1][2]. oauth_allow_insecure_email_lookup This setting is a global configuration option (located in the [auth] section of the configuration file, rather than the [auth.generic_oauth] section) that allows Grafana to use the user's email address as a unique identifier to map OAuth identities to existing users [3]. Default value: false [3]. Behavior: It is intended for scenarios where you need to support users authenticating with the same email address across multiple identity providers (e.g., using both Google OAuth and GitHub OAuth simultaneously) [3]. Security Note: Grafana strongly recommends against enabling this option due to potential security risks [3]. If enabled, ensure that your identity providers are securely configured to prevent email spoofing or unauthorized account takeovers [3]. You can enable oauth_allow_insecure_email_lookup via the configuration file [3], the Grafana HTTP API [3], or through the Grafana UI under Administration -> Authentication -> Auth settings [3].

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.")
PY

Repository: cozystack/website

Length of output: 267


Handle the transition to an empty users[] list.

When spec.oidc.users[] becomes empty, the users-Job is omitted, so stale Grafana org memberships are not pruned. The chart also stops forcing allow_sign_up=false; Grafana defaults this option to true, so an authorized OIDC identity can create a Viewer account again. Run cleanup on the empty-list transition and keep allow_sign_up=false, or document manual revocation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
at line 50, The OIDC users reconciliation documentation must cover the
transition from a non-empty users[] list to an empty list: describe how stale
Grafana memberships are cleaned up and how allow_sign_up remains false, or
explicitly document the required manual revocation and configuration steps.
Update the users-reconcile Job behavior description accordingly, preserving the
existing non-empty-list reconciliation details.

Source: 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.md

Repository: cozystack/website

Length of output: 27436


🌐 Web query:

Grafana oauth_allow_insecure_email_lookup configuration section official documentation

💡 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 oauth_allow_insecure_email_lookup in [auth].

Grafana reads this option only from the global [auth] section, not [auth.generic_oauth]. Keep skip_org_role_sync and allow_sign_up in [auth.generic_oauth].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
around lines 62 - 65, Update the configuration example so
oauth_allow_insecure_email_lookup is rendered under the global [auth] section,
while keeping skip_org_role_sync and allow_sign_up under [auth.generic_oauth].

Source: 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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' -print

Repository: 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 300

Repository: 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 300

Repository: cozystack/website

Length of output: 10615


🌐 Web query:

Grafana official documentation oauth_allow_insecure_email_lookup account takeover email lookup security

💡 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:

OpenID Connect Core official email_verified sub claim stable identifier email mutable

💡 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 CustomConfig.

When spec.oidc.users[] is non-empty, oauth_allow_insecure_email_lookup=true links the OIDC identity to a Grafana account by email. OIDC does not guarantee that email values are unique or stable, and email_verified only confirms verification at issuance. Require a unique, validated, non-editable email claim for BYO IdPs, or map users by (iss, sub) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
around lines 62 - 68, Update the CustomConfig guidance for non-empty
spec.oidc.users so email-based OIDC account linking is permitted only with a
unique, validated, non-editable email claim from a trusted IdP; otherwise
configure identity mapping by (iss, sub) instead. Do not present
oauth_allow_insecure_email_lookup=true as sufficient based solely on
email_verified.

Source: 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Warn against storing client_secret in inline CR configuration.

The example places the OAuth client credential directly in spec.oidc.customConfig.config. Add a warning to use secretRef for production, or provide a Secret-backed client-secret field. The safer Secret path is described later but is not recommended where the credential is introduced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
around lines 82 - 90, Update the OIDC configuration example around
customConfig.config to warn that client_secret must not be stored inline in
production. Direct users to use secretRef or the documented Secret-backed
client-secret field, and place the warning next to the credential where it is
introduced.

Comment on lines +89 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 250

Repository: cozystack/website

Length of output: 50375


🌐 Web query:

OpenID Connect Core 1.0 standard scopes openid profile email groups scope provider-specific claims official documentation

💡 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 groups provider-specific.

groups is not an OpenID Connect Core scope. Document that the identity provider must define this scope or configure a group-claim mapper. Operators must adjust scopes and groups_attribute_path to match that provider. ( )

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
around lines 89 - 90, Update the OIDC configuration example and surrounding
documentation to identify groups as provider-specific rather than an OpenID
Connect Core scope. State that the identity provider must define the groups
scope or provide a group-claim mapper, and instruct operators to align scopes
and groups_attribute_path with their provider.

Source: 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 || true

Repository: cozystack/website

Length of output: 50376


🌐 Web query:

Grafana Generic OAuth groups_attribute_path allowed_groups empty groups login rejected official documentation

💡 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 allowed_groups failure description.

Without groups_attribute_path, Grafana extracts no groups. When allowed_groups is configured, Grafana rejects the login because the extracted group list is empty. It does not treat allowed_groups as a no-op.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/en/docs/v1.6/operations/services/monitoring/oidc-authentication.md`
around lines 151 - 152, Update the `groups_attribute_path` guidance in the OIDC
authentication documentation to state that without it Grafana extracts an empty
group list and rejects logins when `allowed_groups` is configured. Remove the
inaccurate claim that `allowed_groups` becomes a silent no-op, while preserving
the existing configuration guidance.

Source: 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.
Loading