Authentication#
A fresh install has exactly one account: the admin, with a generated password.
That is fine behind kubectl port-forward and not fine the moment Grafana is reachable — every datasource behind it reads every metric in Thanos and every log in the tenant.
The chart does not model authentication itself.
It passes grafana.ini through verbatim, so any authentication Grafana supports is configurable from values, and Grafana’s own documentation stays the reference.
This page covers the wiring that is specific to running Grafana from this chart; follow the links for the provider details.
Where the configuration goes#
Everything lives under grafana.grafana.ini, which deep-merges over the chart’s own keys.
Section names carry dots, which is fine — they are map keys, not paths:
grafana:
grafana.ini:
auth.generic_oauth:
enabled: true| Provider | grafana.ini section | Reference |
|---|---|---|
| Generic OIDC / OAuth2 (Okta, Auth0, Keycloak, Ory, Entra ID) | auth.generic_oauth | Generic OAuth |
| GitHub, GitLab, Google, Azure AD, Okta | auth.github, auth.gitlab, auth.google, auth.azuread, auth.okta | Configure authentication |
| SAML | auth.saml | SAML — Grafana Enterprise / Cloud only |
| LDAP | auth.ldap plus the subchart’s ldap block | LDAP |
| Reverse proxy / mesh-terminated | auth.proxy | Auth proxy |
| Anonymous | auth.anonymous | Anonymous access — refused at render time on an exposed Grafana |
Prefer the generic OIDC section over a provider-specific one unless you need something only the latter has. It works against every IdP that speaks OIDC, and it moves cleanly when the IdP changes.
Client secrets never go in grafana.ini#
grafana.ini renders into a ConfigMap.
A secret written there is plaintext in the release manifest, in helm get values, and in whatever Git repo holds your values file.
Grafana reads secrets from disk or the environment instead:
$__file{/path} | reads a mounted Secret at startup — the better default, and the same mechanism the database password uses |
$__env{VAR} | reads an environment variable injected with grafana.envValueFrom |
The subchart’s assertNoLeakedSecrets check fails the render if a known-sensitive key is set to a literal, including every client_secret.
Leave it on.
The chart does not create the Secret — provision it with External Secrets Operator, Vault Agent, SOPS, or your cloud’s CSI driver.
It must exist in the namespace the Grafana pod runs in, which under split-namespace is grafana, not the release namespace.
A complete OIDC example#
grafana:
grafana.ini:
server:
# Must match the URL users reach. The OAuth redirect URI is built from it,
# and a mismatch fails the callback rather than the login.
root_url: https://grafana.example.com
auth:
# Send users straight to the IdP instead of showing the login form.
oauth_auto_login: true
disable_login_form: true
auth.generic_oauth:
enabled: true
name: SSO
allow_sign_up: true
client_id: <client-id>
client_secret: $__file{/etc/secrets/grafana-oidc/client-secret}
scopes: openid profile email groups
auth_url: https://idp.example.com/oauth2/v1/authorize
token_url: https://idp.example.com/oauth2/v1/token
api_url: https://idp.example.com/oauth2/v1/userinfo
allowed_domains: example.com
# Group membership decides the role, so nobody is provisioned by hand.
role_attribute_path: contains(groups[*], 'sre') && 'Admin' || 'Viewer'
extraSecretMounts:
- name: grafana-oidc
secretName: grafana-oidc
mountPath: /etc/secrets/grafana-oidc
readOnly: trueRegister https://grafana.example.com/login/generic_oauth as the redirect URI with the IdP.
User provisioning is role mapping#
Grafana creates a user on first login when allow_sign_up is true; there is no separate provisioning step to run.
What you do have to decide is what role that user gets, and the answer should be an IdP group rather than a manual grant.
role_attribute_path is a JMESPath expression over the claims returned by api_url, evaluating to Viewer, Editor, Admin, or GrafanaAdmin:
contains(groups[*], 'platform-admins') && 'Admin' || contains(groups[*], 'oncall') && 'Editor' || 'Viewer'Two things that catch people out:
- The claim has to be in the response.
groupsis only present if thegroupsscope is requested and the IdP is configured to emit it. If the expression evaluates to nothing, Grafana falls back toauto_assign_org_role(Viewerby default), which looks like the mapping silently not working. GrafanaAdminneedsallow_assign_grafana_admin: trueas well, or the expression can never grant it.
Set role_attribute_strict: true once the mapping is right — it refuses the login rather than falling back to the default role, which turns a misconfigured claim into a visible failure instead of a quiet privilege change.
For finer-grained control than the four built-in roles, see role-based access control — Grafana Enterprise and Cloud only.
Keep a break-glass path#
disable_login_form: true hides the form; it does not remove it.
https://grafana.example.com/login?disableAutoLogin still reaches it, and the generated admin still works.
Know that before you need it — an IdP outage with no local login is an outage of your incident tooling during an incident. The admin password is in a Secret the chart or the Terraform module owns:
kubectl --namespace monitoring get secret grafana -o jsonpath="{.data.admin-password}" | base64 -dWhat Grafana roles do not do#
Grafana’s roles govern the Grafana UI, not the data behind it. Every datasource is queryable by anyone who can reach Grafana, so a Viewer still reads every metric in Thanos and every log in the tenant — the Explore view alone is enough.
The hard isolation boundary is the install, not the role. See Tenancy & auth for what that means for the logging backend, and Datasources for what each one exposes.
See also#
- Reaching Grafana — expose it before you need any of this, and TLS before that.
- Production Best Practices > Grafana — the checklist form.
- Configure authentication (official) — every provider, in depth.
- Configure Grafana (official) — every
grafana.inikey.