feat(platform): make the Keycloak realm name configurable (#4237)

## What this PR does

Fixes #4216. The Keycloak realm name `cozy` was hardcoded across every
SSO component, so operators could not rename the realm without patching
vendored templates that `make update` reverts. The kube-apiserver OIDC
issuer is already configurable out-of-band via talm `oidcIssuerUrl`,
which made the mismatch worse: pointing the apiserver at a custom realm
left the dashboard, linstor-gui and the generated kubeconfig on `cozy`,
so SSO broke with an issuer mismatch.

This adds a single `authentication.oidc.realmName` (default `cozy`) to
the platform Package values and threads it through the `_cluster`
channel so every consumer resolves the same realm: keycloak-configure
(realm, clients and `cozystack-cluster-admin` roles), the dashboard and
linstor-gui gatekeepers (issuer/login/logout URLs), the info kubeconfig,
and — beyond the components listed in the issue — the Grafana and
managed-Kubernetes apiserver OIDC issuers, which carried the same
hardcoded `/realms/cozy` in their `_helpers.tpl` and would otherwise
have left SSO half-broken.

`keycloakInternalUrl` is now derived from `realmName` when left unset,
so the realm is named in one place. Its default changed from the literal
in-cluster URL to `null` with a documented tri-state: `null` derives
`http://keycloak-http.cozy-keycloak.svc:8080/realms/<realmName>`; `""`
keeps the existing escape hatch that restores external OIDC discovery;
any explicit value is used verbatim. A stock cluster renders the
byte-identical `keycloak-internal-url` it did before, so existing
clusters are unaffected on upgrade.

The Kubernetes CR object names (`keycloakrealm-cozy`, `keycloak-cozy`)
and the `realmRef` that clients point at are internal resource names,
not the realm's technical name, and stay fixed — the EDP operator
resolves clients into the renamed realm through the unchanged
`realmRef`.

Covered by helm-unittest across all seven affected packages, written
test-first (red → green, mutation-verified): default stays `cozy`, a
custom realm reaches every URL, the derived internal URL follows
`realmName`, the `""` escape hatch survives a custom realm, and a
numeric/reserved realm name stays a string. `packages/extra/info` had no
unit-test target, so one was added.

### Downstream repositories

`cozystack/website` needs a follow-up:
`content/en/docs/next/operations/configuration/platform-package.md` is a
hand-written table of the `spec.components.platform.values.*` keys and
does not yet list `authentication.oidc.realmName`. Leaving the box
unticked for a maintainer to action rather than filing a speculative PR.

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: document `authentication.oidc.realmName` in
`platform-package.md`
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
feat(platform): the Keycloak realm name is now configurable via `authentication.oidc.realmName` (default `cozy`), threaded consistently through the apiserver OIDC issuer, dashboard, linstor-gui, Grafana, the managed-Kubernetes apiserver and the generated kubeconfig. Set it to the same realm as the talm `oidcIssuerUrl`. The `authentication.oidc.keycloakInternalUrl` default changed from the literal in-cluster URL to `null`, which now derives `http://keycloak-http.cozy-keycloak.svc:8080/realms/<realmName>`; clusters that leave it unset render the identical URL as before, and `""` still selects external OIDC discovery.
```



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added configurable Keycloak realm support across OIDC integrations,
including Kubernetes, dashboards, monitoring, kubeconfig, and
authentication services.
* The realm defaults to `cozy` and can be customized through platform
authentication settings.
* Internal Keycloak URLs are derived from the configured realm when not
explicitly provided.
* Added realm-name validation and support for explicitly disabling
derived internal URLs.

* **Documentation**
* Updated OIDC guidance to describe configurable realm behavior and
defaults.

* **Tests**
* Added coverage for custom realms, defaults, validation, and generated
OIDC URLs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
scooby87 authored and GitHub committed 2026-09-21 17:05:34 +03:00
commit 6cd78b0e30
28 files changed
+469 -26

No files matched your search

+1 -1
View File
@@ -226,7 +226,7 @@ type OIDC struct {
// Tenant-supplied AuthenticationConfiguration; consumed only when `mode: CustomConfig`.
// +kubebuilder:default:={}
CustomConfig OIDCCustomConfig `json:"customConfig,omitempty"`
// Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform `cozy` realm via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.
// Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform Keycloak realm (`cozy` by default) via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.
// +kubebuilder:default:="None"
Mode OIDCMode `json:"mode"`
// Users granted access to the tenant cluster; each entry produces one ClusterRoleBinding inside the tenant cluster. Works for both `System` and `CustomConfig` modes.
+1 -1
View File
@@ -78,7 +78,7 @@ CR:
fallback, same pattern as `packages/system/dashboard`), preserved
across upgrades.
4. The Grafana CR's `spec.config.auth.generic_oauth` section wired to
the cozy realm issuer + the per-instance audience scope. Two
the platform realm issuer (`cozy` by default, `authentication.oidc.realmName`) + the per-instance audience scope. Two
independent gates ride on top of the raw OAuth wiring:
**Tenant-membership gate — unconditional in System mode.** The chart
+1 -3
View File
@@ -34,9 +34,7 @@ Three modes:
`<release>-admin-kubeconfig` Secret (the Kamaji-minted `super-admin.svc`
kubeconfig). This is the default; existing clusters render byte-identical
to before.
- **System** — the apiserver trusts the platform `cozy` Keycloak realm via a
per-cluster public client. Authenticates the users already in `cozy` (the
realm cozystack ships with). Zero-config default.
- **System** — the apiserver trusts the platform Keycloak realm (`cozy` by default, set via `authentication.oidc.realmName`) via a per-cluster public client. Authenticates the users already in that realm (the one cozystack provisions). Zero-config default.
- **CustomConfig** — the apiserver trusts a tenant-supplied OIDC issuer
directly. `cozy` is not in the path. Use for BYO IdPs (Okta, Auth0, a
customer's own Keycloak).
+1 -1
View File
@@ -207,7 +207,7 @@ See the reference for components utilized in this service:
| Name | Description | Type | Value |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------ |
| `oidc` | OIDC authentication and per-user RBAC for the tenant kube-apiserver. See docs/oidc-tenant.md for the operator guide. | `object` | `{}` |
| `oidc.mode` | Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform `cozy` realm via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path. | `string` | `None` |
| `oidc.mode` | Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform Keycloak realm (`cozy` by default) via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path. | `string` | `None` |
| `oidc.customConfig` | Tenant-supplied AuthenticationConfiguration; consumed only when `mode: CustomConfig`. | `object` | `{}` |
| `oidc.customConfig.config` | Inline AuthenticationConfiguration YAML (`apiserver.config.k8s.io/v1beta1`). The chart writes the value verbatim into a Secret mounted at `/etc/kubernetes/authentication-config/config.yaml` on the kube-apiserver. Mutually exclusive with `secretRef.name`. | `string` | `""` |
| `oidc.customConfig.secretRef` | Reference to an existing Secret in the tenant namespace carrying the `AuthenticationConfiguration`. | `object` | `{}` |
@@ -146,7 +146,7 @@ Issuer URL for `mode: System`. Resolves to the platform Keycloak realm
cozystack-basics.
*/}}
{{- define "kubernetes.oidc.systemIssuerURL" -}}
{{- printf "https://keycloak.%s/realms/cozy" (dig "root-host" "" (.Values._cluster | default dict)) }}
{{- printf "https://keycloak.%s/realms/%s" (dig "root-host" "" (.Values._cluster | default dict)) (dig "oidc-realm-name" "cozy" (.Values._cluster | default dict) | toString) }}
{{- end }}
{{/*
@@ -31,6 +31,7 @@
{{- $issuer := include "kubernetes.oidc.systemIssuerURL" . | trim }}
{{- $audience := include "kubernetes.oidc.clientId" . | trim }}
{{- $groupsExpr := include "kubernetes.oidc.groupsClaimValidationExpr" . | trim }}
{{- $realm := dig "oidc-realm-name" "cozy" (.Values._cluster | default dict) | toString }}
{{- /*
User identity claim: `email`, not `preferred_username`.
@@ -59,7 +60,7 @@ chart) to be present in the token's `groups` claim. Symmetric with the
Grafana-side `allowed_groups` gate in the Monitoring chart — see
docs/oidc-tenant.md and docs/oidc-grafana.md.
*/ -}}
{{- $authnConfig := printf "apiVersion: apiserver.config.k8s.io/v1beta1\nkind: AuthenticationConfiguration\njwt:\n- issuer:\n url: %s\n audiences:\n - %s\n claimValidationRules:\n - expression: '%s'\n message: \"user is not a member of any %s-{view,use,admin,super-admin} group in the cozy realm — this tenant's apiserver only trusts identities the tenant chart has admitted\"\n claimMappings:\n username:\n claim: email\n prefix: \"\"\n groups:\n claim: groups\n prefix: \"\"\n" $issuer $audience $groupsExpr .Release.Namespace }}
{{- $authnConfig := printf "apiVersion: apiserver.config.k8s.io/v1beta1\nkind: AuthenticationConfiguration\njwt:\n- issuer:\n url: %s\n audiences:\n - %s\n claimValidationRules:\n - expression: '%s'\n message: \"user is not a member of any %s-{view,use,admin,super-admin} group in the %s realm — this tenant's apiserver only trusts identities the tenant chart has admitted\"\n claimMappings:\n username:\n claim: email\n prefix: \"\"\n groups:\n claim: groups\n prefix: \"\"\n" $issuer $audience $groupsExpr .Release.Namespace $realm }}
---
apiVersion: v1
kind: Secret
@@ -570,3 +570,33 @@ tests:
asserts:
- failedTemplate:
errorMessage: "spec.oidc.customConfig: set exactly one of `config` (inline) or `secretRef.name` — they are mutually exclusive."
# The managed cluster's apiserver trusts the platform Keycloak realm, whose
# name is driven by _cluster.oidc-realm-name (authentication.oidc.realmName in
# the platform). A renamed realm must reach the AuthenticationConfiguration
# issuer, or the guest apiserver rejects every token the platform mints.
- it: System mode threads a custom realm name into the issuer
release:
name: kubernetes-app
namespace: tenant-foo
template: templates/oidc-authn-config.yaml
set:
_cluster.oidc-enabled: "true"
_cluster.root-host: example.org
_cluster.oidc-realm-name: acme
oidc.mode: System
asserts:
- matchRegex:
path: stringData["config.yaml"]
pattern: "url: https://keycloak.example.org/realms/acme"
- notMatchRegex:
path: stringData["config.yaml"]
pattern: "/realms/cozy"
# The claimValidationRules denial message names the realm; on a renamed
# realm it must not keep telling the user to look in "cozy".
- matchRegex:
path: stringData["config.yaml"]
pattern: "group in the acme realm"
- notMatchRegex:
path: stringData["config.yaml"]
pattern: "group in the cozy realm"
+1 -1
View File
@@ -830,7 +830,7 @@
}
},
"mode": {
"description": "Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform `cozy` realm via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.",
"description": "Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform Keycloak realm (`cozy` by default) via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.",
"type": "string",
"default": "None",
"enum": [
+1 -1
View File
@@ -284,7 +284,7 @@ talos:
## @field {OIDCSecretRef} [secretRef] - Reference to an existing Secret in the tenant namespace carrying the `AuthenticationConfiguration`.
## @typedef {struct} OIDC - OIDC selector for the tenant cluster (Phase 1 — per-cluster audience binding; no per-tenant Keycloak realm).
## @field {OIDCMode} mode - Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform `cozy` realm via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.
## @field {OIDCMode} mode - Identity mode. `None`: no OIDC, only the static admin kubeconfig works. `System`: trust the platform Keycloak realm (`cozy` by default) via a per-cluster public client with audience binding; zero-config default. `CustomConfig`: trust a tenant-supplied issuer directly (BYO); `cozy` is not in the path.
## @field {OIDCCustomConfig} [customConfig] - Tenant-supplied AuthenticationConfiguration; consumed only when `mode: CustomConfig`.
## @field {[]OIDCUser} [users] - Users granted access to the tenant cluster; each entry produces one ClusterRoleBinding inside the tenant cluster. Works for both `System` and `CustomConfig` modes.
+34 -1
View File
@@ -106,7 +106,40 @@ stringData:
oidc-enabled: {{ .Values.authentication.oidc.enabled | quote }}
oidc-insecure-skip-verify: {{ .Values.authentication.oidc.insecureSkipVerify | quote }}
extra-keycloak-redirect-uri-for-dashboard: {{ index .Values.authentication.oidc.keycloakExtraRedirectUri | quote }}
keycloak-internal-url: {{ .Values.authentication.oidc.keycloakInternalUrl | quote }}
{{- /* The realm name becomes a Keycloak realm, a URL path segment in every
issuer/redirect/logout URL, and a shell token in the update-email
hook. Reject anything YAML or --set did not resolve to a string (a
bare number, or true/false) — coercing it would rename the realm to
something never typed and inject a "%!s(...)" into the derived URL —
and reject anything outside a safe slug charset, so a stray "/" or
space cannot split a URL path or the shell. Fail at the source
rather than emit a broken issuer later. The hint does not echo the
parsed value back: YAML has already lost the operator's original
token (on -> true), so quoting is the fix, not the reprinted bool. */}}
{{- /* Unset (null) falls back to cozy; an explicit "" is a meaningless
value, not a default request, so let it reach the charset guard
below rather than swallowing it — mirrors the keycloakInternalUrl
tri-state a few lines down (kindIs "invalid" is null, not ""). */}}
{{- $realmName := .Values.authentication.oidc.realmName }}
{{- if kindIs "invalid" $realmName }}
{{- $realmName = "cozy" }}
{{- end }}
{{- if not (kindIs "string" $realmName) }}
{{- fail (printf "authentication.oidc.realmName must be a string (got %s); wrap the value in quotes in the platform values, e.g. realmName: \"my-realm\"" (kindOf $realmName)) }}
{{- end }}
{{- if not (regexMatch "^[a-zA-Z0-9][a-zA-Z0-9._-]*$" $realmName) }}
{{- fail (printf "authentication.oidc.realmName %q is invalid: use only letters, digits, '.', '_' and '-' (it is used verbatim as a URL path segment and a shell token)" $realmName) }}
{{- end }}
oidc-realm-name: {{ $realmName | quote }}
{{- /* null keycloakInternalUrl derives the in-cluster Service URL for the
configured realm; "" is the external-discovery escape hatch and an
explicit value is passed through, so distinguish null from "" with
kindIs rather than `default`, which would also fire on "". */}}
{{- $keycloakInternalUrl := .Values.authentication.oidc.keycloakInternalUrl }}
{{- if kindIs "invalid" $keycloakInternalUrl }}
{{- $keycloakInternalUrl = printf "http://keycloak-http.cozy-keycloak.svc:8080/realms/%s" $realmName }}
{{- end }}
keycloak-internal-url: {{ $keycloakInternalUrl | quote }}
expose-services: {{ .Values.publishing.exposedServices | join "," | quote }}
{{- /*
expose-ingress carries two semantically distinct values that
@@ -51,3 +51,27 @@ tests:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'keycloak-internal-url:\s*""'
# The default internal URL bakes the realm segment, so it must follow
# authentication.oidc.realmName rather than the literal "cozy" — otherwise a
# renamed realm points every backend call at a realm Keycloak does not serve.
- it: derives the default internal URL from a custom realm name
set:
authentication.oidc.enabled: true
authentication.oidc.realmName: acme
asserts:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'keycloak-internal-url:\s*"http://keycloak-http\.cozy-keycloak\.svc:8080/realms/acme"'
# The escape hatch is an operator decision independent of the realm name, so a
# custom realm must not resurrect a derived URL over an explicit empty string.
- it: keeps the empty-string escape hatch even with a custom realm name
set:
authentication.oidc.enabled: true
authentication.oidc.realmName: acme
authentication.oidc.keycloakInternalUrl: ""
asserts:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'keycloak-internal-url:\s*""'
@@ -0,0 +1,90 @@
suite: apps.yaml OIDC realm-name platform → tenant value wiring
templates:
- templates/apps.yaml
release:
name: cozy-platform
namespace: cozy-system
# authentication.oidc.realmName is the single source for the Keycloak realm.
# It rides the _cluster channel as oidc-realm-name and is read by
# keycloak-configure (realm/clients/roles), the dashboard and linstor-gui
# gatekeepers (issuer/login/logout URLs) and the info kubeconfig. The apiserver
# OIDC issuer is configured out-of-band via talm oidcIssuerUrl, so a mismatch
# there silently breaks SSO; pin the default and the propagation so a rename in
# one place cannot drift from the value every consumer reads.
tests:
- it: defaults the realm name to cozy
asserts:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'oidc-realm-name:\s*"cozy"'
# Unset (null) falls back to cozy, but an explicit empty string is a
# meaningless value, not a request for the default — it is rejected by the
# charset guard rather than silently coerced, mirroring the null-vs-"" tristate
# of keycloakInternalUrl below.
- it: rejects an explicitly empty realm name
set:
authentication.oidc.realmName: ""
asserts:
- failedTemplate:
errorMessage: 'authentication.oidc.realmName "" is invalid: use only letters, digits, ''.'', ''_'' and ''-'' (it is used verbatim as a URL path segment and a shell token)'
- it: propagates a custom realm name onto the _cluster channel
set:
authentication.oidc.realmName: acme
asserts:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'oidc-realm-name:\s*"acme"'
- it: derives the default in-cluster keycloak URL from the realm name
set:
authentication.oidc.realmName: acme
asserts:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'keycloak-internal-url:\s*"http://keycloak-http\.cozy-keycloak\.svc:8080/realms/acme"'
# A realm name YAML resolves to a non-string (a bare integer, or a reserved
# word like on/off that becomes a bool) is rejected, not silently coerced —
# coercion would rename the realm to something never typed and inject a
# "%!s(...)" format error into the derived URL.
- it: rejects a non-string realm name with a quoting hint
set:
authentication.oidc.realmName: 123
asserts:
- failedTemplate:
errorMessage: 'authentication.oidc.realmName must be a string (got float64); wrap the value in quotes in the platform values, e.g. realmName: "my-realm"'
# A quoted numeric name is a legitimate string and passes through to both the
# realm key and the derived URL unchanged.
- it: accepts a quoted numeric realm name verbatim
set:
authentication.oidc.realmName: "123"
asserts:
- matchRegex:
path: stringData["values.yaml"]
pattern: 'oidc-realm-name:\s*"123"'
- matchRegex:
path: stringData["values.yaml"]
pattern: 'keycloak-internal-url:\s*"http://keycloak-http\.cozy-keycloak\.svc:8080/realms/123"'
# The realm is spliced verbatim into URL path segments and a shell token, so a
# name with a "/" or a space must be rejected at the source rather than split
# a URL or break the hook script downstream.
- it: rejects a realm name with a path separator
set:
authentication.oidc.realmName: "foo/bar"
asserts:
- failedTemplate:
errorMessage: 'authentication.oidc.realmName "foo/bar" is invalid: use only letters, digits, ''.'', ''_'' and ''-'' (it is used verbatim as a URL path segment and a shell token)'
- it: rejects a realm name with a space
set:
authentication.oidc.realmName: "foo bar"
asserts:
- failedTemplate:
errorMessage: 'authentication.oidc.realmName "foo bar" is invalid: use only letters, digits, ''.'', ''_'' and ''-'' (it is used verbatim as a URL path segment and a shell token)'
+21 -1
View File
@@ -456,6 +456,20 @@ authentication:
enabled: false
insecureSkipVerify: false
keycloakExtraRedirectUri: ""
# Technical name of the Keycloak realm that owns the cozystack clients and
# the cozystack-cluster-admin roles. keycloak-configure creates the realm
# under this name, and every SSO consumer (dashboard, linstor-gui, the
# generated kubeconfig, and the derived keycloakInternalUrl below) points at
# /realms/<realmName>. The apiserver OIDC issuer is set out-of-band via talm
# oidcIssuerUrl and must carry the same realm, or the tokens minted here are
# rejected. Rename it in one place; keep it in step with talm.
#
# Set this at install time. Changing it on a running cluster does not rename
# the realm: the keycloak-operator provisions a fresh, empty realm under the
# new name and abandons the old one, so every account, session and client
# living in the previous realm is orphaned. Treat a rename on a live cluster
# as a from-scratch re-provision of identities, not an in-place migration.
realmName: "cozy"
# In-cluster URL of the KeyCloak realm, used for backend-to-backend requests. oauth2-proxy runs
# with --skip-oidc-discovery and sends every backend call (token, jwks, userinfo, logout) to this
# URL, while browser-facing redirects keep pointing at the external host.
@@ -470,7 +484,13 @@ authentication:
#
# Override only for a KeyCloak that does not live at the standard in-cluster address. Setting it
# to an empty string restores full OIDC discovery against the external issuer.
keycloakInternalUrl: "http://keycloak-http.cozy-keycloak.svc:8080/realms/cozy"
#
# Left unset (null), it is derived from realmName as
# http://keycloak-http.cozy-keycloak.svc:8080/realms/<realmName>, so a realm rename reaches these
# backend calls automatically. The tri-state is: null -> derive from realmName; "" -> external
# OIDC discovery; any other value -> used verbatim. An explicit value is not rewritten, so a
# custom realm paired with an explicit URL must name the same realm.
keycloakInternalUrl: null
# Gateway API configuration
gateway:
# Enable Gateway API support across the platform. When true:
+4 -1
View File
@@ -1,7 +1,10 @@
NAME=etcd
NAME=info
include ../../../hack/package.mk
test:
helm unittest .
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
yq -o json -i '.properties = {}' values.schema.json
@@ -1,4 +1,5 @@
{{- $host := index .Values._cluster "root-host" }}
{{- $realm := index .Values._cluster "oidc-realm-name" | default "cozy" }}
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
{{- if $oidcEnabled }}
{{- $apiServerEndpoint := index .Values._cluster "api-server-endpoint" }}
@@ -35,7 +36,7 @@ stringData:
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://keycloak.{{ $host }}/realms/cozy
- --oidc-issuer-url=https://keycloak.{{ $host }}/realms/{{ $realm }}
- --oidc-client-id=kubernetes
- --skip-open-browser
command: kubectl
@@ -0,0 +1,43 @@
suite: info kubeconfig OIDC issuer realm
templates:
- templates/kubeconfig.yaml
release:
name: info
namespace: tenant-root
# The generated user kubeconfig runs kubectl oidc-login against
# keycloak.<host>/realms/<realm>. The realm segment is driven by
# _cluster.oidc-realm-name (authentication.oidc.realmName in the platform), so a
# renamed realm must reach the issuer here too — the token this kubeconfig mints
# is rejected by the apiserver unless its issuer matches the apiserver OIDC
# issuer, which the operator points at the same custom realm via talm.
tests:
- it: defaults the issuer realm to cozy
set:
_cluster:
root-host: example.org
oidc-enabled: "true"
api-server-endpoint: https://api.example.org:6443
kube-root-ca: dGVzdA==
asserts:
- matchRegex:
path: stringData.kubeconfig
pattern: '--oidc-issuer-url=https://keycloak\.example\.org/realms/cozy'
- it: threads a custom realm name into the issuer
set:
_cluster:
root-host: example.org
oidc-enabled: "true"
oidc-realm-name: acme
api-server-endpoint: https://api.example.org:6443
kube-root-ca: dGVzdA==
asserts:
- matchRegex:
path: stringData.kubeconfig
pattern: '--oidc-issuer-url=https://keycloak\.example\.org/realms/acme'
- notMatchRegex:
path: stringData.kubeconfig
pattern: '/realms/cozy'
@@ -1,4 +1,5 @@
{{- $host := index .Values._cluster "root-host" }}
{{- $realm := index .Values._cluster "oidc-realm-name" | default "cozy" }}
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
{{- $oidcInsecureSkipVerify := index .Values._cluster "oidc-insecure-skip-verify" }}
{{- $keycloakInternalUrl := index .Values._cluster "keycloak-internal-url" | default "" }}
@@ -55,16 +56,16 @@ spec:
- --upstream=http://cozy-dashboard-console.{{ .Release.Namespace }}.svc:8080
- --http-address=0.0.0.0:8000
- --redirect-url=https://dashboard.{{ $host }}/oauth2/callback
- --oidc-issuer-url=https://keycloak.{{ $host }}/realms/cozy
- --oidc-issuer-url=https://keycloak.{{ $host }}/realms/{{ $realm }}
{{- if $keycloakInternalUrl }}
- --skip-oidc-discovery
- --login-url=https://keycloak.{{ $host }}/realms/cozy/protocol/openid-connect/auth
- --login-url=https://keycloak.{{ $host }}/realms/{{ $realm }}/protocol/openid-connect/auth
- --redeem-url={{ $keycloakInternalUrl }}/protocol/openid-connect/token
- --oidc-jwks-url={{ $keycloakInternalUrl }}/protocol/openid-connect/certs
- --validate-url={{ $keycloakInternalUrl }}/protocol/openid-connect/userinfo
- --backend-logout-url={{ $keycloakInternalUrl }}/protocol/openid-connect/logout?id_token_hint={id_token}
{{- else }}
- --backend-logout-url=https://keycloak.{{ $host }}/realms/cozy/protocol/openid-connect/logout?id_token_hint={id_token}
- --backend-logout-url=https://keycloak.{{ $host }}/realms/{{ $realm }}/protocol/openid-connect/logout?id_token_hint={id_token}
{{- end }}
- --whitelist-domain=keycloak.{{ $host }}
- --email-domain=*
@@ -205,3 +205,48 @@ tests:
- notContains:
path: spec.template.spec.containers[0].args
content: --backend-logout-url=http://keycloak-http.cozy-keycloak.svc:8080/realms/cozy/protocol/openid-connect/logout?id_token_hint={id_token}
# The realm segment of the browser-facing issuer/login/logout URLs is driven
# by _cluster.oidc-realm-name (authentication.oidc.realmName in the platform).
# A renamed realm must reach every one of them; a URL left on the literal
# "cozy" would send the browser to a realm the apiserver OIDC issuer no longer
# matches, silently breaking SSO.
- it: threads a custom realm name through the browser-facing OIDC URLs
set:
_cluster:
root-host: example.org
oidc-enabled: "true"
oidc-realm-name: acme
keycloak-internal-url: http://keycloak-http.cozy-keycloak.svc:8080/realms/acme
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: --oidc-issuer-url=https://keycloak.example.org/realms/acme
- contains:
path: spec.template.spec.containers[0].args
content: --login-url=https://keycloak.example.org/realms/acme/protocol/openid-connect/auth
- notContains:
path: spec.template.spec.containers[0].args
content: --oidc-issuer-url=https://keycloak.example.org/realms/cozy
# The external-discovery branch (empty internal URL) builds its own
# backend-logout-url on the browser host, so the realm segment there must
# follow oidc-realm-name too; reverting $realm in that branch alone would
# otherwise survive the suite.
- it: threads a custom realm into the external-discovery logout URL
set:
_cluster:
root-host: example.org
oidc-enabled: "true"
oidc-realm-name: acme
keycloak-internal-url: ""
asserts:
- notContains:
path: spec.template.spec.containers[0].args
content: --skip-oidc-discovery
- contains:
path: spec.template.spec.containers[0].args
content: --backend-logout-url=https://keycloak.example.org/realms/acme/protocol/openid-connect/logout?id_token_hint={id_token}
- notContains:
path: spec.template.spec.containers[0].args
content: --backend-logout-url=https://keycloak.example.org/realms/cozy/protocol/openid-connect/logout?id_token_hint={id_token}
@@ -1,4 +1,5 @@
{{- $host := index .Values._cluster "root-host" }}
{{- $realmName := index .Values._cluster "oidc-realm-name" | default "cozy" }}
{{- $extraRedirectUris := splitList "," ((index .Values._cluster "extra-keycloak-redirect-uri-for-dashboard") | default "") }}
{{- $k8sCa := index .Values._cluster "kube-root-ca" }}
@@ -25,7 +26,7 @@ metadata:
name: keycloakrealm-cozy
namespace: {{ .Release.Namespace }}
spec:
realmName: cozy
realmName: {{ $realmName | quote }}
clusterKeycloakRef: keycloak-cozy
{{- if $brandingConfig }}
{{- if hasKey $brandingConfig "brandName" }}
@@ -1,5 +1,7 @@
{{- $realmName := index .Values._cluster "oidc-realm-name" | default "cozy" | toString }}
{{/*
Enable the UPDATE_EMAIL required action in the `cozy` realm.
Enable the UPDATE_EMAIL required action in the platform realm (cozy by
default, authentication.oidc.realmName).
The portal user-settings page lets a user change their email via a
Keycloak `kc_action=UPDATE_EMAIL` redirect. That flow only works when
@@ -73,7 +75,7 @@ spec:
set -eu
KCADM=/opt/keycloak/bin/kcadm.sh
CFG=/tmp/kcadm.config
REALM=cozy
REALM={{ $realmName | squote }}
# The realm is reconciled asynchronously by the
# keycloak-operator, so wait for it before configuring it.
@@ -0,0 +1,35 @@
suite: enable-update-email hook Job targets the configured realm
templates:
- templates/enable-update-email.yaml
release:
name: keycloak-configure
namespace: cozy-keycloak
# The enable-update-email hook Job is a consumer of the realm in the same chart
# that creates it: it waits on and configures the realm by name via kcadm. On a
# custom realm it must target the realm keycloak-configure actually provisions,
# not the literal "cozy" — otherwise the Job waits for a realm that never
# appears and the cold install times out, so a non-default realmName is the one
# value nobody could set without breaking the install.
tests:
# The realm is single-quoted in the shell assignment so a name carrying a
# space or a shell metacharacter cannot break or inject into the Job script.
- it: defaults the hook Job realm to cozy
asserts:
- matchRegex:
path: spec.template.spec.containers[0].command[2]
pattern: "REALM='cozy'(\\s|$)"
- it: threads a single-quoted custom realm name into the hook Job
set:
_cluster:
oidc-realm-name: acme
asserts:
- matchRegex:
path: spec.template.spec.containers[0].command[2]
pattern: "REALM='acme'(\\s|$)"
- notMatchRegex:
path: spec.template.spec.containers[0].command[2]
pattern: "REALM='cozy'"
@@ -0,0 +1,49 @@
suite: configure-kk realm name is driven by _cluster.oidc-realm-name
templates:
- templates/configure-kk.yaml
release:
name: keycloak-configure
namespace: cozy-keycloak
# configure-kk.yaml is the source of truth for the realm: it creates the
# ClusterKeycloakRealm, its clients and the cozystack-cluster-admin roles. The
# realm's technical name must follow authentication.oidc.realmName (arriving as
# _cluster.oidc-realm-name from the platform) so operators can rename it in one
# place. The KeycloakRealm CR object name (keycloakrealm-cozy) and the realmRef
# that clients point at are internal Kubernetes names and stay fixed.
tests:
- it: defaults the realm name to cozy when _cluster is absent
asserts:
- matchRegex:
path: spec.realmName
pattern: '^cozy$'
documentIndex: 1
- it: uses a custom realm name from the _cluster channel
set:
_cluster:
oidc-realm-name: acme
asserts:
- equal:
path: spec.realmName
value: acme
documentIndex: 1
# A realm name that is a bare integer or a YAML-reserved word (123, on, no)
# is legal in Keycloak but, rendered unquoted, YAML-coerces to int/bool and
# fails the CRD's string schema on apply — far from values.yaml and hard to
# diagnose. Keep spec.realmName a string regardless of the value's shape.
- it: keeps a numeric realm name a string
set:
_cluster:
oidc-realm-name: 123
asserts:
- equal:
path: spec.realmName
value: "123"
documentIndex: 1
- isKind:
of: ClusterKeycloakRealm
documentIndex: 1
+1 -1
View File
@@ -32,7 +32,7 @@ spec:
singular: kubernetes
plural: kuberneteses
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"storageClass":{"description":"Default StorageClass inside the tenant cluster. Remote-accessible LINSTOR classes are auto-propagated to the tenant under the same name.","type":"string","default":"replicated","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}],"x-cozystack-options":{"source":"storageclass"}},"version":{"description":"Kubernetes major.minor version to deploy","type":"string","default":"v1.35","enum":["v1.35","v1.34","v1.33","v1.32","v1.31"]},"host":{"description":"External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.","type":"string","default":""},"addons":{"description":"Cluster addons configuration.","type":"object","default":{},"required":["certManager","cilium","coredns","gatewayAPI","gpuOperator","hami","ingressNginx","monitoringAgents","ouroboros","velero","verticalPodAutoscaler"],"properties":{"certManager":{"description":"Cert-manager addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable cert-manager.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"cilium":{"description":"Cilium CNI plugin.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"coredns":{"description":"CoreDNS addon.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"gatewayAPI":{"description":"Gateway API addon.","type":"object","default":{},"required":["enabled"],"properties":{"enabled":{"description":"Enable Gateway API.","type":"boolean","default":false}}},"gpuOperator":{"description":"NVIDIA GPU Operator.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable GPU Operator.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"hami":{"description":"HAMi GPU virtualization middleware.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable HAMi (requires GPU Operator).","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"ingressNginx":{"description":"Ingress-NGINX controller.","type":"object","default":{},"required":["enabled","exposeMethod","valuesOverride"],"properties":{"enabled":{"description":"Enable the controller (requires nodes labeled `ingress-nginx`).","type":"boolean","default":false},"exposeMethod":{"description":"Method to expose the controller. Allowed values: `Proxied`, `LoadBalancer`.","type":"string","default":"Proxied","enum":["Proxied","LoadBalancer"]},"hosts":{"description":"Domains routed to this tenant cluster when `exposeMethod` is `Proxied`.","type":"array","default":[],"items":{"type":"string"}},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"monitoringAgents":{"description":"Monitoring agents.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable monitoring agents.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"ouroboros":{"description":"Hairpin-NAT fix for ingress-nginx with PROXY-protocol.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable ouroboros. Requires addons.ingressNginx.enabled (chart-render fail otherwise). Only useful when PROXY-protocol is wired on the tenant ingress-nginx via valuesOverride.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides. Operator-key wins over cozystack defaults.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"velero":{"description":"Velero backup/restore addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable Velero.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"verticalPodAutoscaler":{"description":"Vertical Pod Autoscaler.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"descriptiLine truncated
{"title":"Chart Values","type":"object","properties":{"storageClass":{"description":"Default StorageClass inside the tenant cluster. Remote-accessible LINSTOR classes are auto-propagated to the tenant under the same name.","type":"string","default":"replicated","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}],"x-cozystack-options":{"source":"storageclass"}},"version":{"description":"Kubernetes major.minor version to deploy","type":"string","default":"v1.35","enum":["v1.35","v1.34","v1.33","v1.32","v1.31"]},"host":{"description":"External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.","type":"string","default":""},"addons":{"description":"Cluster addons configuration.","type":"object","default":{},"required":["certManager","cilium","coredns","gatewayAPI","gpuOperator","hami","ingressNginx","monitoringAgents","ouroboros","velero","verticalPodAutoscaler"],"properties":{"certManager":{"description":"Cert-manager addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable cert-manager.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"cilium":{"description":"Cilium CNI plugin.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"coredns":{"description":"CoreDNS addon.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"gatewayAPI":{"description":"Gateway API addon.","type":"object","default":{},"required":["enabled"],"properties":{"enabled":{"description":"Enable Gateway API.","type":"boolean","default":false}}},"gpuOperator":{"description":"NVIDIA GPU Operator.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable GPU Operator.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"hami":{"description":"HAMi GPU virtualization middleware.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable HAMi (requires GPU Operator).","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"ingressNginx":{"description":"Ingress-NGINX controller.","type":"object","default":{},"required":["enabled","exposeMethod","valuesOverride"],"properties":{"enabled":{"description":"Enable the controller (requires nodes labeled `ingress-nginx`).","type":"boolean","default":false},"exposeMethod":{"description":"Method to expose the controller. Allowed values: `Proxied`, `LoadBalancer`.","type":"string","default":"Proxied","enum":["Proxied","LoadBalancer"]},"hosts":{"description":"Domains routed to this tenant cluster when `exposeMethod` is `Proxied`.","type":"array","default":[],"items":{"type":"string"}},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"monitoringAgents":{"description":"Monitoring agents.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable monitoring agents.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"ouroboros":{"description":"Hairpin-NAT fix for ingress-nginx with PROXY-protocol.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable ouroboros. Requires addons.ingressNginx.enabled (chart-render fail otherwise). Only useful when PROXY-protocol is wired on the tenant ingress-nginx via valuesOverride.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides. Operator-key wins over cozystack defaults.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"velero":{"description":"Velero backup/restore addon.","type":"object","default":{},"required":["enabled","valuesOverride"],"properties":{"enabled":{"description":"Enable Velero.","type":"boolean","default":false},"valuesOverride":{"description":"Custom Helm values overrides.","type":"object","default":{},"x-kubernetes-preserve-unknown-fields":true}}},"verticalPodAutoscaler":{"description":"Vertical Pod Autoscaler.","type":"object","default":{},"required":["valuesOverride"],"properties":{"valuesOverride":{"descriptiLine truncated
release:
prefix: kubernetes-
labels:
@@ -1,4 +1,5 @@
{{- $host := index .Values._cluster "root-host" }}
{{- $realm := index .Values._cluster "oidc-realm-name" | default "cozy" }}
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
{{- $oidcInsecureSkipVerify := index .Values._cluster "oidc-insecure-skip-verify" }}
{{- $keycloakInternalUrl := index .Values._cluster "keycloak-internal-url" | default "" }}
@@ -69,16 +70,16 @@ spec:
- --upstream=http://linstor-gui.{{ .Release.Namespace }}.svc:80
- --http-address=0.0.0.0:8000
- --redirect-url=https://linstor-gui.{{ $host }}/oauth2/callback
- --oidc-issuer-url=https://keycloak.{{ $host }}/realms/cozy
- --oidc-issuer-url=https://keycloak.{{ $host }}/realms/{{ $realm }}
{{- if $keycloakInternalUrl }}
- --skip-oidc-discovery
- --login-url=https://keycloak.{{ $host }}/realms/cozy/protocol/openid-connect/auth
- --login-url=https://keycloak.{{ $host }}/realms/{{ $realm }}/protocol/openid-connect/auth
- --redeem-url={{ $keycloakInternalUrl }}/protocol/openid-connect/token
- --oidc-jwks-url={{ $keycloakInternalUrl }}/protocol/openid-connect/certs
- --validate-url={{ $keycloakInternalUrl }}/protocol/openid-connect/userinfo
- --backend-logout-url={{ $keycloakInternalUrl }}/protocol/openid-connect/logout?id_token_hint={id_token}
{{- else }}
- --backend-logout-url=https://keycloak.{{ $host }}/realms/cozy/protocol/openid-connect/logout?id_token_hint={id_token}
- --backend-logout-url=https://keycloak.{{ $host }}/realms/{{ $realm }}/protocol/openid-connect/logout?id_token_hint={id_token}
{{- end }}
- --whitelist-domain=keycloak.{{ $host }}
- --email-domain=*
@@ -298,3 +298,50 @@ tests:
- documentIndex: 1
isKind:
of: Secret
# The realm segment of the browser-facing issuer/login/logout URLs is driven
# by _cluster.oidc-realm-name (authentication.oidc.realmName in the platform).
# A renamed realm must reach every one of them so SSO keeps matching the
# apiserver OIDC issuer.
- it: threads a custom realm name through the browser-facing OIDC URLs
template: templates/gatekeeper.yaml
set:
_cluster:
root-host: dev10.example.org
oidc-enabled: "true"
oidc-realm-name: acme
keycloak-internal-url: http://keycloak-http.cozy-keycloak.svc:8080/realms/acme
expose-services: "linstor-gui"
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: --oidc-issuer-url=https://keycloak.dev10.example.org/realms/acme
- contains:
path: spec.template.spec.containers[0].args
content: --login-url=https://keycloak.dev10.example.org/realms/acme/protocol/openid-connect/auth
- notContains:
path: spec.template.spec.containers[0].args
content: --oidc-issuer-url=https://keycloak.dev10.example.org/realms/cozy
# The external-discovery branch (empty internal URL) builds its own
# backend-logout-url on the browser host; its realm segment must follow
# oidc-realm-name too.
- it: threads a custom realm into the external-discovery logout URL
template: templates/gatekeeper.yaml
set:
_cluster:
root-host: dev10.example.org
oidc-enabled: "true"
oidc-realm-name: acme
keycloak-internal-url: ""
expose-services: "linstor-gui"
asserts:
- notContains:
path: spec.template.spec.containers[0].args
content: --skip-oidc-discovery
- contains:
path: spec.template.spec.containers[0].args
content: --backend-logout-url=https://keycloak.dev10.example.org/realms/acme/protocol/openid-connect/logout?id_token_hint={id_token}
- notContains:
path: spec.template.spec.containers[0].args
content: --backend-logout-url=https://keycloak.dev10.example.org/realms/cozy/protocol/openid-connect/logout?id_token_hint={id_token}
@@ -44,7 +44,8 @@
{{- define "monitoring.oidc.systemIssuerURL" -}}
{{- $host := index .Values._cluster "root-host" -}}
{{- printf "https://keycloak.%s/realms/cozy" $host -}}
{{- $realm := index .Values._cluster "oidc-realm-name" | default "cozy" | toString -}}
{{- printf "https://keycloak.%s/realms/%s" $host $realm -}}
{{- end -}}
{{- /*
@@ -724,3 +724,21 @@ tests:
asserts:
- failedTemplate:
errorMessage: "spec.oidc.users[0].role: \"Superuser\" is not one of the allowed values Admin, Editor, Viewer."
# The grafana OIDC issuer realm is driven by _cluster.oidc-realm-name
# (authentication.oidc.realmName in the platform). A renamed realm must reach
# grafana's auth/token/userinfo URLs, or Grafana SSO breaks against a realm
# Keycloak no longer serves under the old name.
- it: mode=System grafana.yaml threads a custom realm name into the OIDC URLs
template: templates/grafana/grafana.yaml
set:
oidc.mode: System
_cluster.oidc-enabled: "true"
_cluster.oidc-realm-name: acme
oidc.users:
- email: alice@example.com
role: Admin
asserts:
- equal:
path: spec.config["auth.generic_oauth"].auth_url
value: "https://keycloak.example.org/realms/acme/protocol/openid-connect/auth"
+1 -1
View File
@@ -104,7 +104,7 @@ vmagent:
## Identity mode picks the auth source for `[auth.generic_oauth]`:
## None no OIDC; login form + admin_user/admin_password Secret
## stay the only auth path (default).
## System trust the platform `cozy` Keycloak realm via a
## System trust the platform Keycloak realm (`cozy` by default) via a
## per-instance confidential client and audience binding.
## Requires the platform-level authentication.oidc.enabled
## flag; the chart hard-fails the render otherwise.