mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
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:
28 files changed
+469
-26
No files matched your search
Generated
+1
-1
@@ -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.
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
@@ -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": [
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)'
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user