Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions templates/acs-oidc-client-secret-external-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{{- if .Values.keycloak.defaultConfig }}
---
apiVersion: "external-secrets.io/v1beta1"
kind: ExternalSecret
metadata:
name: acs-oidc-client-secret
namespace: {{ .Release.Namespace }}
spec:
refreshInterval: 15s
secretStoreRef:
name: {{ .Values.global.secretStore.name }}
kind: {{ .Values.global.secretStore.kind }}
target:
name: acs-oidc-client-secret
template:
type: Opaque
data:
client-secret: "{{ `{{ .client_secret }}` }}"
data:
- secretKey: client_secret
remoteRef:
key: {{ .Values.keycloak.oidcSecrets.acsClient.vaultPath }}
property: admin-password
{{- end }}
48 changes: 45 additions & 3 deletions templates/keycloak-realm-import.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,50 @@ Merge realms
{{- $realms = append $realms .Values.keycloak.defaultRealm }}
{{- end }}
{{- range $realms }}
{{- $realm := deepCopy . }}
{{- $localDomain := $.Values.global.localClusterDomain }}
{{- $oidcProviderBase := printf "https://spire-spiffe-oidc-discovery-provider.%s" $localDomain }}
{{- if $.Values.keycloak.spiffeIdentityProvider.enabled }}
{{- $spiffeConfig := deepCopy $.Values.keycloak.spiffeIdentityProvider.config }}
{{- $defaultJwksUrl := printf "%s/keys" $oidcProviderBase }}
{{- if or (not (hasKey $spiffeConfig.config "issuer")) (eq (index $spiffeConfig.config "issuer") "") }}
{{- $_ := set $spiffeConfig.config "issuer" $oidcProviderBase }}
{{- end }}
{{- if or (not (hasKey $spiffeConfig.config "jwksUrl")) (eq (index $spiffeConfig.config "jwksUrl") "") }}
{{- $_ := set $spiffeConfig.config "jwksUrl" $defaultJwksUrl }}
{{- end }}
{{- if or (not (hasKey $spiffeConfig.config "authorizationUrl")) (eq (index $spiffeConfig.config "authorizationUrl") "") }}
{{- $_ := set $spiffeConfig.config "authorizationUrl" (printf "%s/authorize" $oidcProviderBase) }}
{{- end }}
{{- if or (not (hasKey $spiffeConfig.config "tokenUrl")) (eq (index $spiffeConfig.config "tokenUrl") "") }}
{{- $_ := set $spiffeConfig.config "tokenUrl" (printf "%s/token" $oidcProviderBase) }}
{{- end }}
{{- $existingIdps := default list $realm.identityProviders }}
{{- $_ := set $realm "identityProviders" (append $existingIdps $spiffeConfig) }}
{{- end }}
{{/* Auto-populate jwt.credential.sub for federated-jwt clients */}}
{{- range $realm.clients }}
{{- if eq (default "" .clientAuthenticatorType) "federated-jwt" }}
{{- $attrs := default dict .attributes }}
{{- if or (not (hasKey $attrs "jwt.credential.sub")) (eq (index $attrs "jwt.credential.sub") "") }}
{{- $clientName := default .clientId .name }}
{{- $_ := set $attrs "jwt.credential.sub" (printf "spiffe://%s/ns/%s/sa/%s" $localDomain $clientName $clientName) }}
{{- end }}
{{- $_ := set . "attributes" $attrs }}
{{- end }}
{{- end }}
---
apiVersion: k8s.keycloak.org/v2alpha1
kind: KeycloakRealmImport
metadata:
name: "{{ .realm }}-realm-import"
name: "{{ $realm.realm }}-realm-import"
namespace: "{{ $.Release.Namespace }}"
annotations:
argocd.argoproj.io/sync-wave: "10"
spec:
keycloakCRName: keycloak
realm:
{{- toYaml . | nindent 4 }}
{{- toYaml $realm | nindent 4 }}
placeholders:
QTODO_ADMIN_PASSWORD:
secret:
Expand All @@ -36,13 +68,23 @@ spec:
secret:
name: {{ $.Values.keycloak.users.secretName }}
key: rhtpa-user-password
{{- if and $.Values.keycloak.oidcSecrets.qtodo (default false $.Values.keycloak.oidcSecrets.qtodo.enabled) }}
QTODO_CLIENT_SECRET:
secret:
name: oidc-client-secret
key: client-secret
{{- end }}
RHTPA_CLI_SECRET:
secret:
name: rhtpa-oidc-cli-secret
key: client-secret
ACS_ADMIN_PASSWORD:
secret:
name: {{ $.Values.keycloak.users.secretName }}
key: acs-admin-password
ACS_CLIENT_SECRET:
secret:
name: acs-oidc-client-secret
key: client-secret
{{- end }}
{{- end }}
{{- end }}
5 changes: 5 additions & 0 deletions templates/keycloak-users-external-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ spec:
qtodo-user1-password: "{{ `{{ .qtodo_user1_password }}` }}"
rhtas-user-password: "{{ `{{ .rhtas_user_password }}` }}"
rhtpa-user-password: "{{ `{{ .rhtpa_user_password }}` }}"
acs-admin-password: "{{ `{{ .acs_admin_password }}` }}"
data:
- secretKey: qtodo_admin_password
remoteRef:
Expand All @@ -35,4 +36,8 @@ spec:
remoteRef:
key: {{ .Values.keycloak.users.passwordVaultKey }}
property: rhtpa-user-password
- secretKey: acs_admin_password
remoteRef:
key: secret/data/hub/infra/acs/acs-central
property: admin-password
{{- end }}
6 changes: 6 additions & 0 deletions templates/keycloak.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ metadata:
annotations:
argocd.argoproj.io/sync-wave: "5"
spec:
{{- if .Values.keycloak.spiffeIdentityProvider.enabled }}
features:
enabled:
- spiffe
- client-auth-federated
{{- end }}
{{- if eq .Values.keycloak.adminUser.enabled true }}
bootstrapAdmin:
user:
Expand Down
2 changes: 1 addition & 1 deletion templates/oidc-client-secret-external-secret.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{- if .Values.keycloak.defaultConfig }}
{{- if and .Values.keycloak.defaultConfig (default false .Values.keycloak.oidcSecrets.qtodo.enabled) }}
apiVersion: "external-secrets.io/v1beta1"
kind: ExternalSecret
metadata:
Expand Down
140 changes: 138 additions & 2 deletions values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,28 @@ keycloak:
name: qtodo
protocol: openid-connect
publicClient: false
clientAuthenticatorType: federated-jwt
serviceAccountsEnabled: true
redirectUris:
- "*"
secret: ${QTODO_CLIENT_SECRET}
standardFlowEnabled: true
directAccessGrantsEnabled: false
webOrigins:
- +
fullScopeAllowed: true
attributes:
jwt.credential.issuer: spiffe
# Auto-generated by template: spiffe://<localClusterDomain>/ns/qtodo/sa/qtodo
jwt.credential.sub: ""
post.logout.redirect.uris: "+"
defaultClientScopes:
- web-origins
- roles
- profile
- basic
- email
optionalClientScopes:
- offline_access
- clientId: trusted-artifact-signer
enabled: true
name: Red Hat Trusted Artifact Signer Client
Expand Down Expand Up @@ -59,6 +75,54 @@ keycloak:
access.token.claim: "true"
id.token.claim: "true"
userinfo.token.claim: "false"
# ACS Central OIDC Client
- clientId: acs-central
enabled: true
name: Red Hat Advanced Cluster Security Central
protocol: openid-connect
publicClient: false
secret: ${ACS_CLIENT_SECRET}
redirectUris:
- "*"
directAccessGrantsEnabled: true
standardFlowEnabled: true
implicitFlowEnabled: false
webOrigins:
- "*"
fullScopeAllowed: true
defaultClientScopes:
- openid
- basic
- email
- profile
- roles
- web-origins
optionalClientScopes:
- address
- phone
- offline_access
protocolMappers:
- name: groups
protocol: openid-connect
protocolMapper: oidc-group-membership-mapper
consentRequired: false
config:
full.path: "false"
id.token.claim: "true"
access.token.claim: "true"
claim.name: groups
userinfo.token.claim: "true"
- name: roles
protocol: openid-connect
protocolMapper: oidc-usermodel-realm-role-mapper
consentRequired: false
config:
multivalued: "true"
userinfo.token.claim: "true"
id.token.claim: "true"
access.token.claim: "true"
claim.name: roles
jsonType.label: String
# RHTPA CLI Client - matches Trustify 'cli' client configuration
# Reference: https://github.com/guacsec/trustify-helm-charts/blob/main/charts/trustify-infrastructure/templates/keycloak/010-ConfigMap.yaml
- clientId: rhtpa-cli
Expand Down Expand Up @@ -132,6 +196,22 @@ keycloak:
# Note: We must define 'basic' scope with 'sub' mapper for OIDC compliance
# The 'sub' claim is required by RHTPA for user identification
clientScopes:
# OpenID scope - mandatory OIDC scope (required by ACS and other OIDC clients)
- name: openid
description: OpenID Connect built-in scope
protocol: openid-connect
attributes:
include.in.token.scope: "true"
display.on.consent.screen: "false"
protocolMappers:
- name: sub
protocol: openid-connect
protocolMapper: oidc-sub-mapper
consentRequired: false
config:
introspection.token.claim: "true"
access.token.claim: "true"
id.token.claim: "true"
# Basic scope - required for 'sub' claim in tokens
# Standard OIDC scopes required by Trustify/RHTPA
# Reference: https://github.com/mrrajan/trustify/blob/doc_rhbk_operator/docs/book/modules/admin/pages/infrastructure.adoc
Expand Down Expand Up @@ -268,6 +348,7 @@ keycloak:
display.on.consent.screen: "false"
# Set default client scopes for the realm (applied to all new clients)
defaultDefaultClientScopes:
- openid
- basic
- email
- profile
Expand All @@ -283,6 +364,8 @@ keycloak:
name: create:sbom
- description: RHTPA Document Creator
name: create:document
- description: ACS Administrator
name: acs-admin
users:
- createdTimestamp: 1
credentials:
Expand Down Expand Up @@ -342,6 +425,20 @@ keycloak:
- create:sbom
- create:document
username: rhtpa-user
- createdTimestamp: 1
credentials:
- temporary: false
type: password
value: ${ACS_ADMIN_PASSWORD}
email: admin@example.com
emailVerified: true
enabled: true
firstName: ACS
lastName: Administrator
realmRoles:
- acs-admin
- offline_access
username: admin
ingress:
enabled: true
service: keycloak-service-trusted
Expand All @@ -364,9 +461,48 @@ keycloak:
secretName: keycloak-users
# OIDC client secrets for realm configuration
oidcSecrets:
# QTodo OIDC client secret (app-level)
# QTodo OIDC client secret — disabled when using federated-jwt (client assertion)
qtodo:
enabled: false
vaultPath: secret/data/apps/qtodo/qtodo-oidc-client
# RHTPA CLI OIDC client secret (infra)
rhtpaCli:
vaultPath: secret/data/hub/infra/rhtpa/rhtpa-oidc-cli
# ACS Central OIDC client secret (infra)
acsClient:
vaultPath: secret/data/hub/infra/acs/acs-central
# SPIFFE Identity Provider for Federated Client Authentication
# Requires RHBK 26.4+ with Technology Preview features: spiffe + client-auth-federated
# (automatically enabled in keycloak.yaml when this is enabled)
#
# Uses an OIDC provider type (not Keycloak's native SPIFFE provider) because the
# ZTWIM operator forces SpireServer.jwtIssuer to be an HTTPS URL, so JWT SVIDs
# contain iss: "https://spire-spiffe-oidc-discovery-provider.<domain>".
# Keycloak's native SPIFFE IdP rejects this (expects spiffe:// URI).
# The OIDC provider matches the HTTPS issuer, enabling Keycloak's federated-jwt
# client authenticator to resolve clients by iss+sub without requiring client_id.
#
# Reference: https://www.keycloak.org/2026/01/federated-client-authentication
spiffeIdentityProvider:
enabled: true
config:
alias: spiffe
displayName: SPIFFE Workload Identity
providerId: oidc
enabled: true
hideOnLogin: true
config:
# SPIRE OIDC Discovery Provider issuer URL (auto-generated if empty)
issuer: ""
# Required by Keycloak OIDC IdP but unused for federated client auth
authorizationUrl: ""
tokenUrl: ""
# SPIRE OIDC Discovery Provider JWKS URL (auto-generated if empty)
jwksUrl: ""
clientId: keycloak
clientSecret: unused
useJwksUrl: "true"
validateSignature: "true"
supportsClientAssertions: "true"
supportsClientAssertionReuse: "true"
syncMode: LEGACY
Loading