Skip to content

Commit 8a5142c

Browse files
feat(monitoring): support externally managed Keycloak
Signed-off-by: Guilherme Steinmuller <gsteinmuller@vexxhost.com>
1 parent 88ca766 commit 8a5142c

7 files changed

Lines changed: 149 additions & 8 deletions

File tree

doc/source/admin/monitoring.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,76 @@ Prometheus as the data source. You can find more examples of how to do
206206
this in the Grafana Helm chart `Import Dashboards <https://github.com/grafana/helm-charts/tree/main/charts/grafana#import-dashboards>`_
207207
documentation.
208208

209+
Externally managed Keycloak
210+
===========================
211+
212+
By default, Atmosphere manages the Keycloak resources needed by monitoring: the
213+
realm, client scope mapper, clients, client roles, and generated client
214+
secrets.
215+
216+
For multi-region environments where Keycloak is shared and owned outside of the
217+
regional monitoring deployment, disable Keycloak management:
218+
219+
.. code-block:: yaml
220+
221+
kube_prometheus_stack_keycloak_manage: false
222+
223+
This does not disable Keycloak authentication for Grafana, Prometheus, or
224+
Alertmanager. It only stops the monitoring role from calling the Keycloak admin
225+
API. The role still waits for ``kube_prometheus_stack_keycloak_server_url`` and
226+
expects the referenced clients and Kubernetes secrets to exist before it deploys
227+
the monitoring resources.
228+
229+
When using a shared Keycloak, override the Keycloak endpoint to point at the
230+
region that owns authentication. You can either set ``keycloak_host`` or override
231+
the full URL:
232+
233+
.. code-block:: yaml
234+
235+
keycloak_host: auth.primary.example.com
236+
# or:
237+
kube_prometheus_stack_keycloak_server_url: https://auth.primary.example.com
238+
239+
You must also set the local monitoring endpoints for the region being deployed:
240+
241+
.. code-block:: yaml
242+
243+
kube_prometheus_stack_alertmanager_host: alertmanager.region.example.com
244+
kube_prometheus_stack_grafana_host: grafana.region.example.com
245+
kube_prometheus_stack_prometheus_host: prometheus.region.example.com
246+
247+
Use region-specific client IDs when the shared Keycloak serves more than one
248+
monitoring deployment:
249+
250+
.. code-block:: yaml
251+
252+
kube_prometheus_stack_alertmanager_client_id: alertmanager-region
253+
kube_prometheus_stack_grafana_client_id: grafana-region
254+
kube_prometheus_stack_prometheus_client_id: prometheus-region
255+
256+
Each client must have a Kubernetes ``Secret`` in
257+
``kube_prometheus_stack_helm_release_namespace`` containing a ``password`` key.
258+
The namespace defaults to ``monitoring``. Override the secret names when they do
259+
not match the role defaults:
260+
261+
.. code-block:: yaml
262+
263+
kube_prometheus_stack_alertmanager_client_secret_name: alertmanager-region-client-secret
264+
kube_prometheus_stack_grafana_client_secret_name: grafana-region-client-secret
265+
kube_prometheus_stack_prometheus_client_secret_name: prometheus-region-client-secret
266+
267+
The shared Keycloak must already contain matching clients, client roles, client
268+
secrets, and redirect URIs. Alertmanager and Prometheus require a ``member``
269+
client role. Grafana requires ``admin``, ``editor``, and ``viewer`` client
270+
roles. The role mapper that exposes client roles in the token must also exist.
271+
272+
The required redirect URIs are based on the endpoint variables:
273+
274+
- ``https://{{ kube_prometheus_stack_alertmanager_host }}/oauth2/callback``
275+
- ``https://{{ kube_prometheus_stack_grafana_host }}/login``
276+
- ``https://{{ kube_prometheus_stack_grafana_host }}/login/generic_oauth``
277+
- ``https://{{ kube_prometheus_stack_prometheus_host }}/oauth2/callback``
278+
209279
************
210280
Viewing data
211281
************
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
features:
3+
- |
4+
The ``kube_prometheus_stack`` role can now consume externally managed
5+
Keycloak clients by setting ``kube_prometheus_stack_keycloak_manage`` to
6+
``false``. In this mode the role waits for the configured Keycloak API but
7+
does not create or update Keycloak realms, clients, or roles, and instead
8+
validates that the expected Kubernetes client secret objects already exist.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
# `kube_prometheus_stack`
2+
3+
See the
4+
[monitoring documentation](../../doc/source/admin/monitoring.rst)
5+
for operator-facing configuration, including externally managed Keycloak
6+
clients.

roles/kube_prometheus_stack/defaults/main.yml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ kube_prometheus_stack_prometheus_ingress_annotations:
5353
cert-manager.io/common-name: "{{ kube_prometheus_stack_prometheus_host }}"
5454
kube_prometheus_stack_prometheus_tls_template: "{{ _kube_prometheus_stack_tls_template }}"
5555

56+
kube_prometheus_stack_keycloak_manage: true
5657
kube_prometheus_stack_keycloak_server_url: "https://{{ keycloak_host }}"
5758
kube_prometheus_stack_keycloak_admin_realm_name: master
5859
kube_prometheus_stack_keycloak_admin_client_id: admin-cli
@@ -61,20 +62,33 @@ kube_prometheus_stack_keycloak_admin_password: "{{ keycloak_admin_password }}"
6162
kube_prometheus_stack_keycloak_realm: atmosphere
6263
kube_prometheus_stack_keycloak_realm_name: Atmosphere
6364

65+
kube_prometheus_stack_alertmanager_client_id: alertmanager
66+
kube_prometheus_stack_alertmanager_client_secret_name: >-
67+
{{ kube_prometheus_stack_helm_release_name }}-{{ kube_prometheus_stack_alertmanager_client_id }}-client-secret
68+
kube_prometheus_stack_grafana_client_id: grafana
69+
kube_prometheus_stack_grafana_client_secret_name: >-
70+
{{ kube_prometheus_stack_helm_release_name }}-{{ kube_prometheus_stack_grafana_client_id }}-client-secret
71+
kube_prometheus_stack_prometheus_client_id: prometheus
72+
kube_prometheus_stack_prometheus_client_secret_name: >-
73+
{{ kube_prometheus_stack_helm_release_name }}-{{ kube_prometheus_stack_prometheus_client_id }}-client-secret
74+
6475
kube_prometheus_stack_keycloak_clients:
65-
- id: alertmanager
76+
- id: "{{ kube_prometheus_stack_alertmanager_client_id }}"
77+
client_secret_name: "{{ kube_prometheus_stack_alertmanager_client_secret_name }}"
6678
port: 9093
6779
roles: ["member"]
6880
oauth2_proxy: true
6981
redirect_uris:
7082
- "https://{{ kube_prometheus_stack_alertmanager_host }}/oauth2/callback"
71-
- id: grafana
83+
- id: "{{ kube_prometheus_stack_grafana_client_id }}"
84+
client_secret_name: "{{ kube_prometheus_stack_grafana_client_secret_name }}"
7285
roles: ["admin", "editor", "viewer"]
7386
oauth2_proxy: false
7487
redirect_uris:
7588
- "https://{{ kube_prometheus_stack_grafana_host }}/login"
7689
- "https://{{ kube_prometheus_stack_grafana_host }}/login/generic_oauth"
77-
- id: prometheus
90+
- id: "{{ kube_prometheus_stack_prometheus_client_id }}"
91+
client_secret_name: "{{ kube_prometheus_stack_prometheus_client_secret_name }}"
7892
port: 9090
7993
roles: ["member"]
8094
oauth2_proxy: true

roles/kube_prometheus_stack/tasks/main.yml

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
realm: "{{ kube_prometheus_stack_keycloak_realm }}"
4040
display_name: "{{ kube_prometheus_stack_keycloak_realm_name }}"
4141
enabled: true
42+
when: kube_prometheus_stack_keycloak_manage | bool
4243

4344
- name: Add client roles in "id_token"
4445
no_log: true
@@ -64,6 +65,7 @@
6465
access.token.claim: true
6566
id.token.claim: true
6667
multivalued: true
68+
when: kube_prometheus_stack_keycloak_manage | bool
6769

6870
- name: Retrieve "etcd" CA certificate
6971
run_once: true
@@ -111,7 +113,9 @@
111113
apiVersion: secretgen.k14s.io/v1alpha1
112114
kind: Password
113115
metadata:
114-
name: "{{ kube_prometheus_stack_helm_release_name }}-{{ item.id }}-client-secret"
116+
name: >-
117+
{{ item.client_secret_name
118+
| default(kube_prometheus_stack_helm_release_name ~ '-' ~ item.id ~ '-client-secret') }}
115119
namespace: "{{ kube_prometheus_stack_helm_release_namespace }}"
116120
spec:
117121
length: 64
@@ -124,6 +128,7 @@
124128
loop: "{{ kube_prometheus_stack_keycloak_clients }}"
125129
loop_control:
126130
label: "{{ item.id }}"
131+
when: kube_prometheus_stack_keycloak_manage | bool
127132

128133
- name: Collect all client secrets
129134
run_once: true
@@ -136,6 +141,28 @@
136141
loop_control:
137142
label: "{{ password.item.id }}"
138143
loop_var: password
144+
when: kube_prometheus_stack_keycloak_manage | bool
145+
146+
- name: Validate externally managed client secrets
147+
run_once: true
148+
kubernetes.core.k8s_info:
149+
kind: Secret
150+
namespace: "{{ kube_prometheus_stack_helm_release_namespace }}"
151+
name: >-
152+
{{ item.client_secret_name
153+
| default(kube_prometheus_stack_helm_release_name ~ '-' ~ item.id ~ '-client-secret') }}
154+
register: kube_prometheus_stack_external_client_secret
155+
failed_when: >-
156+
kube_prometheus_stack_external_client_secret.resources | length == 0 or
157+
kube_prometheus_stack_external_client_secret.resources[0].data.password
158+
is not defined
159+
loop: "{{ kube_prometheus_stack_keycloak_clients }}"
160+
loop_control:
161+
label: >-
162+
{{ item.id }}
163+
({{ item.client_secret_name
164+
| default(kube_prometheus_stack_helm_release_name ~ '-' ~ item.id ~ '-client-secret') }})
165+
when: not (kube_prometheus_stack_keycloak_manage | bool)
139166

140167
- name: Create Keycloak clients
141168
no_log: true
@@ -165,6 +192,7 @@
165192
loop_control:
166193
label: "{{ secret.password.item.id }}"
167194
loop_var: secret
195+
when: kube_prometheus_stack_keycloak_manage | bool
168196

169197
- name: Create Keycloak roles
170198
no_log: true
@@ -184,6 +212,7 @@
184212
loop: "{{ kube_prometheus_stack_keycloak_clients | subelements('roles') }}"
185213
loop_control:
186214
label: "{{ item.0.id }}-{{ item.1 }}"
215+
when: kube_prometheus_stack_keycloak_manage | bool
187216

188217
- name: Generate cookie secrets
189218
run_once: true
@@ -222,7 +251,9 @@
222251
ref:
223252
apiVersion: v1
224253
kind: Secret
225-
name: "{{ kube_prometheus_stack_helm_release_name }}-{{ item.id }}-client-secret"
254+
name: >-
255+
{{ item.client_secret_name
256+
| default(kube_prometheus_stack_helm_release_name ~ '-' ~ item.id ~ '-client-secret') }}
226257
- name: cookie-secret
227258
ref:
228259
apiVersion: v1

roles/kube_prometheus_stack/tasks_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,38 @@ func TestCreateKeycloakRealmTask(t *testing.T) {
4242
require.NotNil(t, task)
4343

4444
assert.Equal(t, true, task["no_log"])
45+
assert.Equal(t, "kube_prometheus_stack_keycloak_manage | bool",
46+
task["when"])
4547
}
4648

4749
func TestAddClientRolesInIdTokenTask(t *testing.T) {
4850
task := getTaskByName("Add client roles in \"id_token\"")
4951
require.NotNil(t, task)
5052

5153
assert.Equal(t, true, task["no_log"])
54+
assert.Equal(t, "kube_prometheus_stack_keycloak_manage | bool",
55+
task["when"])
5256
}
5357

5458
func TestCreateKeycloakClientsTask(t *testing.T) {
5559
task := getTaskByName("Create Keycloak clients")
5660
require.NotNil(t, task)
5761

5862
assert.Equal(t, true, task["no_log"])
63+
assert.Equal(t, "kube_prometheus_stack_keycloak_manage | bool",
64+
task["when"])
5965
}
6066

6167
func TestCreateKeycloakRolesTask(t *testing.T) {
6268
task := getTaskByName("Create Keycloak roles")
6369
require.NotNil(t, task)
6470

6571
assert.Equal(t, true, task["no_log"])
72+
assert.Equal(t, "kube_prometheus_stack_keycloak_manage | bool",
73+
task["when"])
74+
}
75+
76+
func TestValidateExternallyManagedClientSecretsTask(t *testing.T) {
77+
task := getTaskByName("Validate externally managed client secrets")
78+
require.NotNil(t, task)
6679
}

roles/kube_prometheus_stack/vars/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ _kube_prometheus_stack_helm_values:
220220
adminPassword: "{{ kube_prometheus_stack_grafana_admin_password }}"
221221
extraSecretMounts:
222222
- name: auth-generic-oauth-secret-mount
223-
secretName: "{{ kube_prometheus_stack_helm_release_name }}-grafana-client-secret"
223+
secretName: "{{ kube_prometheus_stack_grafana_client_secret_name }}"
224224
defaultMode: "0440"
225225
mountPath: /etc/secrets/auth_generic_oauth
226226
readOnly: true
@@ -236,7 +236,7 @@ _kube_prometheus_stack_helm_values:
236236
enabled: true
237237
name: Atmosphere
238238
allow_sign_up: true
239-
client_id: grafana
239+
client_id: "{{ kube_prometheus_stack_grafana_client_id }}"
240240
client_secret: "$__file{/etc/secrets/auth_generic_oauth/password}"
241241
scopes: openid email profile offline_access roles
242242
email_attribute_path: email
@@ -247,7 +247,7 @@ _kube_prometheus_stack_helm_values:
247247
api_url: "{{ kube_prometheus_stack_keycloak_server_url }}/realms/{{ kube_prometheus_stack_keycloak_realm }}/protocol/openid-connect/userinfo"
248248
tls_skip_verify_insecure: true
249249
# yamllint disable-line rule:line-length
250-
role_attribute_path: "contains(resource_access.grafana.roles[*], 'admin') && 'Admin' || contains(resource_access.grafana.roles[*], 'editor') && 'Editor' || contains(resource_access.grafana.roles[*], 'viewer') && 'Viewer'"
250+
role_attribute_path: "contains(resource_access.\"{{ kube_prometheus_stack_grafana_client_id }}\".roles[*], 'admin') && 'Admin' || contains(resource_access.\"{{ kube_prometheus_stack_grafana_client_id }}\".roles[*], 'editor') && 'Editor' || contains(resource_access.\"{{ kube_prometheus_stack_grafana_client_id }}\".roles[*], 'viewer') && 'Viewer'"
251251
role_attribute_strict: true
252252
skip_org_role_sync: false
253253
image:

0 commit comments

Comments
 (0)