diff --git a/templates/acs-oidc-client-secret-external-secret.yaml b/templates/acs-oidc-client-secret-external-secret.yaml new file mode 100644 index 0000000..77f244c --- /dev/null +++ b/templates/acs-oidc-client-secret-external-secret.yaml @@ -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 }} diff --git a/templates/keycloak-realm-import.yaml b/templates/keycloak-realm-import.yaml index 0ebf52c..56b9993 100644 --- a/templates/keycloak-realm-import.yaml +++ b/templates/keycloak-realm-import.yaml @@ -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: @@ -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 }} \ No newline at end of file diff --git a/templates/keycloak-users-external-secret.yaml b/templates/keycloak-users-external-secret.yaml index ee2bf91..ecf8ec0 100644 --- a/templates/keycloak-users-external-secret.yaml +++ b/templates/keycloak-users-external-secret.yaml @@ -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: @@ -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 }} diff --git a/templates/keycloak.yaml b/templates/keycloak.yaml index a2fd854..c12da17 100644 --- a/templates/keycloak.yaml +++ b/templates/keycloak.yaml @@ -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: diff --git a/templates/oidc-client-secret-external-secret.yaml b/templates/oidc-client-secret-external-secret.yaml index 5f5a5ad..0ba01a6 100644 --- a/templates/oidc-client-secret-external-secret.yaml +++ b/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/values.yaml b/values.yaml index cd2da27..5c7f30c 100644 --- a/values.yaml +++ b/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 @@ -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 @@ -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 @@ -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 @@ -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: @@ -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 @@ -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.". + # 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