diff --git a/charts/keycloak/templates/keycloak-realm-import.yaml b/charts/keycloak/templates/keycloak-realm-import.yaml index c223e522..08ff19b9 100644 --- a/charts/keycloak/templates/keycloak-realm-import.yaml +++ b/charts/keycloak/templates/keycloak-realm-import.yaml @@ -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 @@ -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 diff --git a/charts/keycloak/templates/oidc-client-secret-external-secret.yaml b/charts/keycloak/templates/oidc-client-secret-external-secret.yaml index 5f5a5ad6..0ba01a6a 100644 --- a/charts/keycloak/templates/oidc-client-secret-external-secret.yaml +++ b/charts/keycloak/templates/oidc-client-secret-external-secret.yaml @@ -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: diff --git a/charts/keycloak/values.yaml b/charts/keycloak/values.yaml index 7abd6224..8d15b7c9 100644 --- a/charts/keycloak/values.yaml +++ b/charts/keycloak/values.yaml @@ -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:///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 @@ -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: @@ -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.". + # 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:// + # 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./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: "" \ No newline at end of file + # 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 \ No newline at end of file diff --git a/charts/qtodo/templates/app-config-env.yaml b/charts/qtodo/templates/app-config-env.yaml index 441ce5b1..de978bc2 100644 --- a/charts/qtodo/templates/app-config-env.yaml +++ b/charts/qtodo/templates/app-config-env.yaml @@ -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 }} diff --git a/charts/qtodo/templates/app-deployment.yaml b/charts/qtodo/templates/app-deployment.yaml index 3dfa0ea6..0da099c3 100644 --- a/charts/qtodo/templates/app-deployment.yaml +++ b/charts/qtodo/templates/app-deployment.yaml @@ -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 @@ -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 diff --git a/charts/qtodo/templates/spiffe-helper-config.yaml b/charts/qtodo/templates/spiffe-helper-config.yaml index bd6ac9ec..5a3b42fc 100644 --- a/charts/qtodo/templates/spiffe-helper-config.yaml +++ b/charts/qtodo/templates/spiffe-helper-config.yaml @@ -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: @@ -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 }} \ No newline at end of file diff --git a/charts/qtodo/values.yaml b/charts/qtodo/values.yaml index d5fd70c8..90494f40 100644 --- a/charts/qtodo/values.yaml +++ b/charts/qtodo/values.yaml @@ -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 @@ -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" diff --git a/values-hub.yaml b/values-hub.yaml index a96ff419..bd74ea0d 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -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 diff --git a/values-secret.yaml.template b/values-secret.yaml.template index e1f48ac9..b00aaafe 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -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: