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
32 changes: 26 additions & 6 deletions charts/keycloak/templates/keycloak-realm-import.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,37 @@ Merge realms
{{- 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 }}
{{- $localDomain := $.Values.global.localClusterDomain }}
{{- $defaultIssuer := printf "spiffe://%s" $localDomain }}
{{- $defaultJwksUrl := printf "https://spire-spiffe-oidc-discovery-provider.%s/keys" $localDomain }}
{{- $defaultJwksUrl := printf "%s/keys" $oidcProviderBase }}
{{- if or (not (hasKey $spiffeConfig.config "issuer")) (eq (index $spiffeConfig.config "issuer") "") }}
{{- $_ := set $spiffeConfig.config "issuer" $defaultIssuer }}
{{- $_ := 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 }}
{{- $_ := set $spiffeConfig.config "bundleEndpoint" ($spiffeConfig.config.jwksUrl | default $defaultJwksUrl) }}
{{- $_ := unset $spiffeConfig.config "jwksUrl" }}
{{- $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
Expand Down Expand Up @@ -50,10 +68,12 @@ 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
Expand Down
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
57 changes: 40 additions & 17 deletions charts/keycloak/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 @@ -364,8 +380,9 @@ 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:
Expand All @@ -374,28 +391,34 @@ keycloak:
# Requires RHBK 26.4+ with Technology Preview features: spiffe + client-auth-federated
# (automatically enabled in keycloak.yaml when this is enabled)
#
# Enables two capabilities:
# 1. Federated Client Authentication: Clients authenticate to Keycloak using
# SPIFFE JWT SVIDs instead of client secrets (clientAuthenticatorType: federated-jwt)
# 2. Token Exchange: Workloads exchange SPIFFE JWT SVIDs for Keycloak access tokens
# 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
# Playground: https://github.com/keycloak/keycloak-playground/tree/main/federated-client-authentication/spiffe
spiffeIdentityProvider:
enabled: true
config:
alias: spiffe
displayName: SPIFFE Workload Identity
providerId: spiffe
providerId: oidc
enabled: true
hideOnLogin: true
config:
# SPIFFE trust domain in URI format (auto-generated from global.localClusterDomain if empty)
# Keycloak stores this under the "issuer" config key internally
# Format: spiffe://<localClusterDomain>
# SPIRE OIDC Discovery Provider issuer URL (auto-generated if empty)
issuer: ""
# SPIRE OIDC Discovery Provider JWKS URL (auto-generated from global.localClusterDomain if empty)
# Defaults to: https://spire-spiffe-oidc-discovery-provider.<localClusterDomain>/keys
# Uses standard TLS (OpenShift route) — no custom CA or truststore management needed.
# Note: Keycloak stores this internally as "bundleEndpoint"; we use "jwksUrl" here
# to clearly indicate it targets the OIDC JWKS endpoint, not the SPIRE federation bundle.
jwksUrl: ""
# 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
6 changes: 5 additions & 1 deletion charts/qtodo/templates/app-config-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ metadata:
data:
{{- if eq .Values.app.spire.enabled true }}
QUARKUS_OIDC_ENABLED: "true"
QUARKUS_OIDC_AUTH_SERVER_URL: "{{ default (printf "https://keycloak.%s/realms/ztvp" .Values.global.localClusterDomain) .Values.app.oidc.authServerUrl }}"
QUARKUS_OIDC_AUTH_SERVER_URL: "{{ default (printf "https://keycloak.%s/realms/%s" .Values.global.localClusterDomain .Values.app.keycloak.realm) .Values.app.oidc.authServerUrl }}"
QUARKUS_OIDC_CLIENT_ID: "{{ .Values.app.oidc.clientId }}"
QUARKUS_OIDC_APPLICATION_TYPE: "{{ .Values.app.oidc.applicationType }}"
QUARKUS_HTTP_AUTH_PERMISSION_AUTHENTICATED_PATHS: "{{ .Values.app.oidc.authenticatedPaths }}"
QUARKUS_HTTP_AUTH_PERMISSION_AUTHENTICATED_POLICY: "{{ .Values.app.oidc.authenticatedPolicy }}"
QUARKUS_OIDC_AUTHENTICATION_FORCE_REDIRECT_HTTPS_SCHEME: "true"
{{- if .Values.app.oidc.clientAssertion.enabled }}
QUARKUS_OIDC_CREDENTIALS_JWT_SOURCE: "bearer"
QUARKUS_OIDC_CREDENTIALS_JWT_TOKEN_PATH: "{{ .Values.app.oidc.clientAssertion.jwtTokenPath }}"
{{- end }}
{{- end }}
7 changes: 7 additions & 0 deletions charts/qtodo/templates/app-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,13 @@ spec:
{{- else }}
- name: QUARKUS_CONFIG_LOCATIONS
value: file:/run/secrets/db-credentials/credentials.properties
{{- if not .Values.app.oidc.clientAssertion.enabled }}
- name: QUARKUS_OIDC_CREDENTIALS_SECRET
valueFrom:
secretKeyRef:
name: oidc-client-secret
key: client-secret
{{- end }}
{{- if .Values.app.truststore.enabled }}
# Truststore password from Vault secret
- name: TRUSTSTORE_PASSWORD
Expand All @@ -255,6 +257,11 @@ spec:
volumeMounts:
- name: db-credentials
mountPath: /run/secrets/db-credentials
{{- if .Values.app.oidc.clientAssertion.enabled }}
- name: svids
mountPath: /svids
readOnly: true
{{- end }}
{{- if .Values.app.truststore.enabled }}
- name: truststore
mountPath: /run/secrets/truststore
Expand Down
3 changes: 2 additions & 1 deletion charts/qtodo/templates/spiffe-helper-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- if eq .Values.app.spire.enabled true }}
{{- $keycloakRealmUrl := default (printf "https://keycloak.%s/realms/%s" .Values.global.localClusterDomain .Values.app.keycloak.realm) .Values.app.keycloak.realmUrl }}
kind: ConfigMap
apiVersion: v1
metadata:
Expand All @@ -15,6 +16,6 @@ data:
svid_file_name = "svid.pem"
svid_key_file_name = "svid_key.pem"
svid_bundle_file_name = "svid_bundle.pem"
jwt_svids = [{jwt_audience="qtodo", jwt_svid_file_name="jwt.token"}]
jwt_svids = [{jwt_audience="{{ $keycloakRealmUrl }}", jwt_svid_file_name="jwt.token"}]
jwt_bundle_file_name = "jwt_bundle.json"
{{- end }}
11 changes: 8 additions & 3 deletions charts/qtodo/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,20 @@ app:
applicationType: "web-app"
authenticatedPaths: "/*"
authenticatedPolicy: "authenticated"
# Federated client assertion using SPIFFE JWT SVID
clientAssertion:
enabled: true
jwtTokenPath: "/svids/jwt.token"

# Keycloak realm configuration
keycloak:
realm: "ztvp"
# Keycloak realm URL (auto-generated from global.localClusterDomain and realm if empty)
realmUrl: ""

spire:
enabled: true # Enable SPIFFE + OIDC integration by default
sidecars: true
audiences:
- qtodo

# Vault configuration for SPIFFE integration
# Uses SPIFFE JWT to authenticate and fetch DB password
Expand All @@ -62,8 +66,9 @@ app:
secretPath: "secret/data/apps/qtodo/qtodo-db"

# OIDC External Secret configuration
# Disabled when using federated client assertion (no client secret needed)
oidcSecret:
enabled: true
enabled: false
name: "oidc-client-secret"
# QTodo OIDC secret path (app-level isolation)
vaultPath: "secret/data/apps/qtodo/qtodo-oidc-client"
Expand Down
2 changes: 1 addition & 1 deletion values-hub.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ clusterGroup:
defaultRole: qtodo
roles:
- name: qtodo
audience: qtodo
audience: https://keycloak.apps.{{ $.Values.global.clusterDomain }}/realms/ztvp
subject: spiffe://apps.{{ $.Values.global.clusterDomain }}/ns/qtodo/sa/qtodo
policies:
- apps-qtodo-jwt-secret
Expand Down
16 changes: 9 additions & 7 deletions values-secret.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ secrets:
onMissingValue: generate
vaultPolicy: validatedPatternDefaultPolicy

- name: qtodo-oidc-client
vaultPrefixes:
- apps/qtodo
fields:
- name: client-secret
onMissingValue: generate
vaultPolicy: alphaNumericPolicy
# qtodo-oidc-client secret is no longer needed — qtodo now authenticates
# to Keycloak using SPIFFE JWT SVID (federated client assertion)
#- name: qtodo-oidc-client
# vaultPrefixes:
# - apps/qtodo
# fields:
# - name: client-secret
# onMissingValue: generate
# vaultPolicy: alphaNumericPolicy

- name: qtodo-truststore
vaultPrefixes:
Expand Down