Skip to content

Commit ed21405

Browse files
committed
chore: update keycloak template to be aligned with latest layered-zero-trust submissions.
- Qtodo using federated JWT (SPIFFE) instead of a client secret when oidcSecrets.qtodo.enabled is false. - ACS Central having its own OIDC client and admin user, with secrets coming from Vault and the new ExternalSecrets. Signed-off-by: Min Zhang <minzhang@redhat.com>
1 parent f1f2249 commit ed21405

6 files changed

Lines changed: 219 additions & 6 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{{- if .Values.keycloak.defaultConfig }}
2+
---
3+
apiVersion: "external-secrets.io/v1beta1"
4+
kind: ExternalSecret
5+
metadata:
6+
name: acs-oidc-client-secret
7+
namespace: {{ .Release.Namespace }}
8+
spec:
9+
refreshInterval: 15s
10+
secretStoreRef:
11+
name: {{ .Values.global.secretStore.name }}
12+
kind: {{ .Values.global.secretStore.kind }}
13+
target:
14+
name: acs-oidc-client-secret
15+
template:
16+
type: Opaque
17+
data:
18+
client-secret: "{{ `{{ .client_secret }}` }}"
19+
data:
20+
- secretKey: client_secret
21+
remoteRef:
22+
key: {{ .Values.keycloak.oidcSecrets.acsClient.vaultPath }}
23+
property: admin-password
24+
{{- end }}

templates/keycloak-realm-import.yaml

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,50 @@ Merge realms
77
{{- $realms = append $realms .Values.keycloak.defaultRealm }}
88
{{- end }}
99
{{- range $realms }}
10+
{{- $realm := deepCopy . }}
11+
{{- $localDomain := $.Values.global.localClusterDomain }}
12+
{{- $oidcProviderBase := printf "https://spire-spiffe-oidc-discovery-provider.%s" $localDomain }}
13+
{{- if $.Values.keycloak.spiffeIdentityProvider.enabled }}
14+
{{- $spiffeConfig := deepCopy $.Values.keycloak.spiffeIdentityProvider.config }}
15+
{{- $defaultJwksUrl := printf "%s/keys" $oidcProviderBase }}
16+
{{- if or (not (hasKey $spiffeConfig.config "issuer")) (eq (index $spiffeConfig.config "issuer") "") }}
17+
{{- $_ := set $spiffeConfig.config "issuer" $oidcProviderBase }}
18+
{{- end }}
19+
{{- if or (not (hasKey $spiffeConfig.config "jwksUrl")) (eq (index $spiffeConfig.config "jwksUrl") "") }}
20+
{{- $_ := set $spiffeConfig.config "jwksUrl" $defaultJwksUrl }}
21+
{{- end }}
22+
{{- if or (not (hasKey $spiffeConfig.config "authorizationUrl")) (eq (index $spiffeConfig.config "authorizationUrl") "") }}
23+
{{- $_ := set $spiffeConfig.config "authorizationUrl" (printf "%s/authorize" $oidcProviderBase) }}
24+
{{- end }}
25+
{{- if or (not (hasKey $spiffeConfig.config "tokenUrl")) (eq (index $spiffeConfig.config "tokenUrl") "") }}
26+
{{- $_ := set $spiffeConfig.config "tokenUrl" (printf "%s/token" $oidcProviderBase) }}
27+
{{- end }}
28+
{{- $existingIdps := default list $realm.identityProviders }}
29+
{{- $_ := set $realm "identityProviders" (append $existingIdps $spiffeConfig) }}
30+
{{- end }}
31+
{{/* Auto-populate jwt.credential.sub for federated-jwt clients */}}
32+
{{- range $realm.clients }}
33+
{{- if eq (default "" .clientAuthenticatorType) "federated-jwt" }}
34+
{{- $attrs := default dict .attributes }}
35+
{{- if or (not (hasKey $attrs "jwt.credential.sub")) (eq (index $attrs "jwt.credential.sub") "") }}
36+
{{- $clientName := default .clientId .name }}
37+
{{- $_ := set $attrs "jwt.credential.sub" (printf "spiffe://%s/ns/%s/sa/%s" $localDomain $clientName $clientName) }}
38+
{{- end }}
39+
{{- $_ := set . "attributes" $attrs }}
40+
{{- end }}
41+
{{- end }}
1042
---
1143
apiVersion: k8s.keycloak.org/v2alpha1
1244
kind: KeycloakRealmImport
1345
metadata:
14-
name: "{{ .realm }}-realm-import"
46+
name: "{{ $realm.realm }}-realm-import"
1547
namespace: "{{ $.Release.Namespace }}"
1648
annotations:
1749
argocd.argoproj.io/sync-wave: "10"
1850
spec:
1951
keycloakCRName: keycloak
2052
realm:
21-
{{- toYaml . | nindent 4 }}
53+
{{- toYaml $realm | nindent 4 }}
2254
placeholders:
2355
QTODO_ADMIN_PASSWORD:
2456
secret:
@@ -36,13 +68,23 @@ spec:
3668
secret:
3769
name: {{ $.Values.keycloak.users.secretName }}
3870
key: rhtpa-user-password
71+
{{- if and $.Values.keycloak.oidcSecrets.qtodo (default false $.Values.keycloak.oidcSecrets.qtodo.enabled) }}
3972
QTODO_CLIENT_SECRET:
4073
secret:
4174
name: oidc-client-secret
4275
key: client-secret
76+
{{- end }}
4377
RHTPA_CLI_SECRET:
4478
secret:
4579
name: rhtpa-oidc-cli-secret
4680
key: client-secret
81+
ACS_ADMIN_PASSWORD:
82+
secret:
83+
name: {{ $.Values.keycloak.users.secretName }}
84+
key: acs-admin-password
85+
ACS_CLIENT_SECRET:
86+
secret:
87+
name: acs-oidc-client-secret
88+
key: client-secret
89+
{{- end }}
4790
{{- end }}
48-
{{- end }}

templates/keycloak-users-external-secret.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ spec:
1818
qtodo-user1-password: "{{ `{{ .qtodo_user1_password }}` }}"
1919
rhtas-user-password: "{{ `{{ .rhtas_user_password }}` }}"
2020
rhtpa-user-password: "{{ `{{ .rhtpa_user_password }}` }}"
21+
acs-admin-password: "{{ `{{ .acs_admin_password }}` }}"
2122
data:
2223
- secretKey: qtodo_admin_password
2324
remoteRef:
@@ -35,4 +36,8 @@ spec:
3536
remoteRef:
3637
key: {{ .Values.keycloak.users.passwordVaultKey }}
3738
property: rhtpa-user-password
39+
- secretKey: acs_admin_password
40+
remoteRef:
41+
key: secret/data/hub/infra/acs/acs-central
42+
property: admin-password
3843
{{- end }}

templates/keycloak.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ metadata:
66
annotations:
77
argocd.argoproj.io/sync-wave: "5"
88
spec:
9+
{{- if .Values.keycloak.spiffeIdentityProvider.enabled }}
10+
features:
11+
enabled:
12+
- spiffe
13+
- client-auth-federated
14+
{{- end }}
915
{{- if eq .Values.keycloak.adminUser.enabled true }}
1016
bootstrapAdmin:
1117
user:

templates/oidc-client-secret-external-secret.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{{- if .Values.keycloak.defaultConfig }}
1+
{{- if and .Values.keycloak.defaultConfig (default false .Values.keycloak.oidcSecrets.qtodo.enabled) }}
22
apiVersion: "external-secrets.io/v1beta1"
33
kind: ExternalSecret
44
metadata:

values.yaml

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,28 @@ keycloak:
1818
name: qtodo
1919
protocol: openid-connect
2020
publicClient: false
21+
clientAuthenticatorType: federated-jwt
22+
serviceAccountsEnabled: true
2123
redirectUris:
2224
- "*"
23-
secret: ${QTODO_CLIENT_SECRET}
2425
standardFlowEnabled: true
26+
directAccessGrantsEnabled: false
2527
webOrigins:
2628
- +
29+
fullScopeAllowed: true
30+
attributes:
31+
jwt.credential.issuer: spiffe
32+
# Auto-generated by template: spiffe://<localClusterDomain>/ns/qtodo/sa/qtodo
33+
jwt.credential.sub: ""
34+
post.logout.redirect.uris: "+"
35+
defaultClientScopes:
36+
- web-origins
37+
- roles
38+
- profile
39+
- basic
40+
- email
41+
optionalClientScopes:
42+
- offline_access
2743
- clientId: trusted-artifact-signer
2844
enabled: true
2945
name: Red Hat Trusted Artifact Signer Client
@@ -59,6 +75,54 @@ keycloak:
5975
access.token.claim: "true"
6076
id.token.claim: "true"
6177
userinfo.token.claim: "false"
78+
# ACS Central OIDC Client
79+
- clientId: acs-central
80+
enabled: true
81+
name: Red Hat Advanced Cluster Security Central
82+
protocol: openid-connect
83+
publicClient: false
84+
secret: ${ACS_CLIENT_SECRET}
85+
redirectUris:
86+
- "*"
87+
directAccessGrantsEnabled: true
88+
standardFlowEnabled: true
89+
implicitFlowEnabled: false
90+
webOrigins:
91+
- "*"
92+
fullScopeAllowed: true
93+
defaultClientScopes:
94+
- openid
95+
- basic
96+
- email
97+
- profile
98+
- roles
99+
- web-origins
100+
optionalClientScopes:
101+
- address
102+
- phone
103+
- offline_access
104+
protocolMappers:
105+
- name: groups
106+
protocol: openid-connect
107+
protocolMapper: oidc-group-membership-mapper
108+
consentRequired: false
109+
config:
110+
full.path: "false"
111+
id.token.claim: "true"
112+
access.token.claim: "true"
113+
claim.name: groups
114+
userinfo.token.claim: "true"
115+
- name: roles
116+
protocol: openid-connect
117+
protocolMapper: oidc-usermodel-realm-role-mapper
118+
consentRequired: false
119+
config:
120+
multivalued: "true"
121+
userinfo.token.claim: "true"
122+
id.token.claim: "true"
123+
access.token.claim: "true"
124+
claim.name: roles
125+
jsonType.label: String
62126
# RHTPA CLI Client - matches Trustify 'cli' client configuration
63127
# Reference: https://github.com/guacsec/trustify-helm-charts/blob/main/charts/trustify-infrastructure/templates/keycloak/010-ConfigMap.yaml
64128
- clientId: rhtpa-cli
@@ -132,6 +196,22 @@ keycloak:
132196
# Note: We must define 'basic' scope with 'sub' mapper for OIDC compliance
133197
# The 'sub' claim is required by RHTPA for user identification
134198
clientScopes:
199+
# OpenID scope - mandatory OIDC scope (required by ACS and other OIDC clients)
200+
- name: openid
201+
description: OpenID Connect built-in scope
202+
protocol: openid-connect
203+
attributes:
204+
include.in.token.scope: "true"
205+
display.on.consent.screen: "false"
206+
protocolMappers:
207+
- name: sub
208+
protocol: openid-connect
209+
protocolMapper: oidc-sub-mapper
210+
consentRequired: false
211+
config:
212+
introspection.token.claim: "true"
213+
access.token.claim: "true"
214+
id.token.claim: "true"
135215
# Basic scope - required for 'sub' claim in tokens
136216
# Standard OIDC scopes required by Trustify/RHTPA
137217
# Reference: https://github.com/mrrajan/trustify/blob/doc_rhbk_operator/docs/book/modules/admin/pages/infrastructure.adoc
@@ -268,6 +348,7 @@ keycloak:
268348
display.on.consent.screen: "false"
269349
# Set default client scopes for the realm (applied to all new clients)
270350
defaultDefaultClientScopes:
351+
- openid
271352
- basic
272353
- email
273354
- profile
@@ -283,6 +364,8 @@ keycloak:
283364
name: create:sbom
284365
- description: RHTPA Document Creator
285366
name: create:document
367+
- description: ACS Administrator
368+
name: acs-admin
286369
users:
287370
- createdTimestamp: 1
288371
credentials:
@@ -342,6 +425,20 @@ keycloak:
342425
- create:sbom
343426
- create:document
344427
username: rhtpa-user
428+
- createdTimestamp: 1
429+
credentials:
430+
- temporary: false
431+
type: password
432+
value: ${ACS_ADMIN_PASSWORD}
433+
email: admin@example.com
434+
emailVerified: true
435+
enabled: true
436+
firstName: ACS
437+
lastName: Administrator
438+
realmRoles:
439+
- acs-admin
440+
- offline_access
441+
username: admin
345442
ingress:
346443
enabled: true
347444
service: keycloak-service-trusted
@@ -364,9 +461,48 @@ keycloak:
364461
secretName: keycloak-users
365462
# OIDC client secrets for realm configuration
366463
oidcSecrets:
367-
# QTodo OIDC client secret (app-level)
464+
# QTodo OIDC client secret — disabled when using federated-jwt (client assertion)
368465
qtodo:
466+
enabled: false
369467
vaultPath: secret/data/apps/qtodo/qtodo-oidc-client
370468
# RHTPA CLI OIDC client secret (infra)
371469
rhtpaCli:
372470
vaultPath: secret/data/hub/infra/rhtpa/rhtpa-oidc-cli
471+
# ACS Central OIDC client secret (infra)
472+
acsClient:
473+
vaultPath: secret/data/hub/infra/acs/acs-central
474+
# SPIFFE Identity Provider for Federated Client Authentication
475+
# Requires RHBK 26.4+ with Technology Preview features: spiffe + client-auth-federated
476+
# (automatically enabled in keycloak.yaml when this is enabled)
477+
#
478+
# Uses an OIDC provider type (not Keycloak's native SPIFFE provider) because the
479+
# ZTWIM operator forces SpireServer.jwtIssuer to be an HTTPS URL, so JWT SVIDs
480+
# contain iss: "https://spire-spiffe-oidc-discovery-provider.<domain>".
481+
# Keycloak's native SPIFFE IdP rejects this (expects spiffe:// URI).
482+
# The OIDC provider matches the HTTPS issuer, enabling Keycloak's federated-jwt
483+
# client authenticator to resolve clients by iss+sub without requiring client_id.
484+
#
485+
# Reference: https://www.keycloak.org/2026/01/federated-client-authentication
486+
spiffeIdentityProvider:
487+
enabled: true
488+
config:
489+
alias: spiffe
490+
displayName: SPIFFE Workload Identity
491+
providerId: oidc
492+
enabled: true
493+
hideOnLogin: true
494+
config:
495+
# SPIRE OIDC Discovery Provider issuer URL (auto-generated if empty)
496+
issuer: ""
497+
# Required by Keycloak OIDC IdP but unused for federated client auth
498+
authorizationUrl: ""
499+
tokenUrl: ""
500+
# SPIRE OIDC Discovery Provider JWKS URL (auto-generated if empty)
501+
jwksUrl: ""
502+
clientId: keycloak
503+
clientSecret: unused
504+
useJwksUrl: "true"
505+
validateSignature: "true"
506+
supportsClientAssertions: "true"
507+
supportsClientAssertionReuse: "true"
508+
syncMode: LEGACY

0 commit comments

Comments
 (0)