From dc3d50cac4b0f687c9b6d9c577e7ce5b24b393aa Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Sat, 15 Nov 2025 04:34:22 +0300 Subject: [PATCH] BE: Support PEM trust/key stores and mTLS configuration. (#1437) --- .../ui/client/RetryingKafkaConnectClient.java | 2 + .../kafbat/ui/config/ClustersProperties.java | 14 +++ .../ui/model/MetricsScrapeProperties.java | 17 +++- .../ui/service/AdminClientServiceImpl.java | 7 +- .../ui/service/ConsumerGroupService.java | 7 +- .../ui/service/KafkaClusterFactory.java | 10 +- .../io/kafbat/ui/service/MessagesService.java | 7 +- .../scrape/jmx/JmxMetricsRetriever.java | 8 +- .../scrape/jmx/JmxSslSocketFactory.java | 58 ++++------- .../ui/util/KafkaClientSslPropertiesUtil.java | 23 +++-- .../ui/util/KafkaServicesValidation.java | 38 ++++--- .../java/io/kafbat/ui/util/SslBundleUtil.java | 99 +++++++++++++++++++ .../kafbat/ui/util/WebClientConfigurator.java | 67 +++++-------- contract-typespec/api/config.tsp | 37 +++++-- .../main/resources/swagger/kafbat-ui-api.yaml | 52 +++++++--- .../utils/getInitialFormData.ts | 6 +- 16 files changed, 292 insertions(+), 160 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/util/SslBundleUtil.java diff --git a/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java b/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java index cbe48f811..b1aa649f4 100644 --- a/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java +++ b/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java @@ -342,6 +342,8 @@ public static WebClient buildWebClient(DataSize maxBuffSize, .configureSsl( truststoreConfig, new ClustersProperties.KeystoreConfig( + config.getKeystoreType(), + config.getKeystoreCertificate(), config.getKeystoreLocation(), config.getKeystorePassword() ) diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index e521f85a9..0d862f57d 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -2,6 +2,7 @@ import static io.kafbat.ui.model.MetricsScrapeProperties.JMX_METRICS_TYPE; +import io.kafbat.ui.api.model.SecurityProtocol; import jakarta.annotation.PostConstruct; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -63,7 +64,9 @@ public static class Cluster { @NotBlank(message = "field bootstrapServers for for cluster could not be blank") String bootstrapServers; + SecurityProtocol securityProtocol; TruststoreConfig ssl; + KeystoreConfig kafkaSsl; String schemaRegistry; SchemaRegistryAuth schemaRegistryAuth; @@ -108,6 +111,8 @@ public static class MetricsConfig { Boolean ssl; String username; String password; + StoreType keystoreType; + String keystoreCertificate; String keystoreLocation; String keystorePassword; @@ -143,6 +148,8 @@ public static class ConnectCluster { String address; String username; String password; + StoreType keystoreType; + String keystoreCertificate; String keystoreLocation; String keystorePassword; } @@ -154,9 +161,14 @@ public static class SchemaRegistryAuth { String password; } + public enum StoreType { + JKS, PKCS12, PEM + } + @Data @ToString(exclude = {"truststorePassword"}) public static class TruststoreConfig { + StoreType truststoreType; String truststoreLocation; String truststorePassword; boolean verifySsl = true; @@ -167,6 +179,8 @@ public static class TruststoreConfig { @AllArgsConstructor @ToString(exclude = {"keystorePassword"}) public static class KeystoreConfig { + StoreType keystoreType; + String keystoreCertificate; String keystoreLocation; String keystorePassword; } diff --git a/api/src/main/java/io/kafbat/ui/model/MetricsScrapeProperties.java b/api/src/main/java/io/kafbat/ui/model/MetricsScrapeProperties.java index 171e14e09..1fa67a558 100644 --- a/api/src/main/java/io/kafbat/ui/model/MetricsScrapeProperties.java +++ b/api/src/main/java/io/kafbat/ui/model/MetricsScrapeProperties.java @@ -30,17 +30,24 @@ public class MetricsScrapeProperties { public static MetricsScrapeProperties create(ClustersProperties.Cluster cluster) { var metrics = Objects.requireNonNull(cluster.getMetrics()); + + KeystoreConfig keystoreConfig = null; + if (metrics.getKeystoreLocation() != null) { + keystoreConfig = new KeystoreConfig( + metrics.getKeystoreType(), + metrics.getKeystoreCertificate(), + metrics.getKeystoreLocation(), + metrics.getKeystorePassword() + ); + } + return MetricsScrapeProperties.builder() .port(metrics.getPort()) .ssl(Optional.ofNullable(metrics.getSsl()).orElse(false)) .username(metrics.getUsername()) .password(metrics.getPassword()) .truststoreConfig(cluster.getSsl()) - .keystoreConfig( - metrics.getKeystoreLocation() != null - ? new KeystoreConfig(metrics.getKeystoreLocation(), metrics.getKeystorePassword()) - : null - ) + .keystoreConfig(keystoreConfig) .build(); } diff --git a/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java b/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java index b6ff631b6..a45d307d9 100644 --- a/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java +++ b/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java @@ -45,7 +45,12 @@ public Mono get(KafkaCluster cluster) { private Mono createAdminClient(KafkaCluster cluster) { return Mono.fromSupplier(() -> { Properties properties = new Properties(); - KafkaClientSslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); + KafkaClientSslPropertiesUtil.addKafkaSslProperties( + cluster.getOriginalProperties().getSsl(), + cluster.getOriginalProperties().getKafkaSsl(), + cluster.getOriginalProperties().getSecurityProtocol(), + properties + ); properties.putAll(cluster.getProperties()); properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); properties.putIfAbsent(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, clientTimeout); diff --git a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java index ce8f6a09c..6faefb323 100644 --- a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java +++ b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java @@ -353,7 +353,12 @@ public EnhancedConsumer createConsumer(KafkaCluster cluster) { public EnhancedConsumer createConsumer(KafkaCluster cluster, Map properties) { Properties props = new Properties(); - KafkaClientSslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), props); + KafkaClientSslPropertiesUtil.addKafkaSslProperties( + cluster.getOriginalProperties().getSsl(), + cluster.getOriginalProperties().getKafkaSsl(), + cluster.getOriginalProperties().getSecurityProtocol(), + props + ); props.putAll(cluster.getProperties()); props.putAll(cluster.getConsumerProperties()); props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafbat-ui-consumer-" + System.currentTimeMillis()); diff --git a/api/src/main/java/io/kafbat/ui/service/KafkaClusterFactory.java b/api/src/main/java/io/kafbat/ui/service/KafkaClusterFactory.java index 5a0c5201a..50cbe4dbf 100644 --- a/api/src/main/java/io/kafbat/ui/service/KafkaClusterFactory.java +++ b/api/src/main/java/io/kafbat/ui/service/KafkaClusterFactory.java @@ -4,7 +4,7 @@ import static io.kafbat.ui.util.KafkaServicesValidation.validateKsql; import static io.kafbat.ui.util.KafkaServicesValidation.validatePrometheusStore; import static io.kafbat.ui.util.KafkaServicesValidation.validateSchemaRegistry; -import static io.kafbat.ui.util.KafkaServicesValidation.validateTruststore; +import static io.kafbat.ui.util.KafkaServicesValidation.validateSslBundle; import io.kafbat.ui.client.RetryingKafkaConnectClient; import io.kafbat.ui.config.ClustersProperties; @@ -105,12 +105,12 @@ public KafkaCluster create(ClustersProperties properties, public Mono validate(ClustersProperties.Cluster clusterProperties) { if (clusterProperties.getSsl() != null) { - Optional errMsg = validateTruststore(clusterProperties.getSsl()); + Optional errMsg = validateSslBundle(clusterProperties.getSsl(), clusterProperties.getKafkaSsl()); if (errMsg.isPresent()) { return Mono.just(new ClusterConfigValidationDTO() .kafka(new ApplicationPropertyValidationDTO() .error(true) - .errorMessage("Truststore not valid: " + errMsg.get()))); + .errorMessage("Truststore/Keystore not valid: " + errMsg.get()))); } } @@ -118,7 +118,9 @@ public Mono validate(ClustersProperties.Cluster clus validateClusterConnection( clusterProperties.getBootstrapServers(), convertProperties(clusterProperties.getProperties()), - clusterProperties.getSsl() + clusterProperties.getSsl(), + clusterProperties.getKafkaSsl(), + clusterProperties.getSecurityProtocol() ), schemaRegistryConfigured(clusterProperties) ? validateSchemaRegistry(() -> schemaRegistryClient(clusterProperties)).map(Optional::of) diff --git a/api/src/main/java/io/kafbat/ui/service/MessagesService.java b/api/src/main/java/io/kafbat/ui/service/MessagesService.java index 3bf3d8330..652107d7c 100644 --- a/api/src/main/java/io/kafbat/ui/service/MessagesService.java +++ b/api/src/main/java/io/kafbat/ui/service/MessagesService.java @@ -204,7 +204,12 @@ public static KafkaProducer createProducer(KafkaCluster cluster, public static KafkaProducer createProducer(ClustersProperties.Cluster cluster, Map additionalProps) { Properties properties = new Properties(); - KafkaClientSslPropertiesUtil.addKafkaSslProperties(cluster.getSsl(), properties); + KafkaClientSslPropertiesUtil.addKafkaSslProperties( + cluster.getSsl(), + cluster.getKafkaSsl(), + cluster.getSecurityProtocol(), + properties + ); properties.putAll(cluster.getProperties()); properties.putAll(cluster.getProducerProperties()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxMetricsRetriever.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxMetricsRetriever.java index e5984725d..f62c8b6ac 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxMetricsRetriever.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxMetricsRetriever.java @@ -105,12 +105,7 @@ private Map prepareJmxEnvAndSetThreadLocal(MetricsScrapeProperti if (isSslJmxEndpoint(scrapeProperties)) { var truststoreConfig = scrapeProperties.getTruststoreConfig(); var keystoreConfig = scrapeProperties.getKeystoreConfig(); - JmxSslSocketFactory.setSslContextThreadLocal( - truststoreConfig != null ? truststoreConfig.getTruststoreLocation() : null, - truststoreConfig != null ? truststoreConfig.getTruststorePassword() : null, - keystoreConfig != null ? keystoreConfig.getKeystoreLocation() : null, - keystoreConfig != null ? keystoreConfig.getKeystorePassword() : null - ); + JmxSslSocketFactory.setSslContextThreadLocal(truststoreConfig, keystoreConfig); JmxSslSocketFactory.editJmxConnectorEnv(env); } @@ -144,4 +139,3 @@ private List extractObjectMetrics(ObjectName objectName, MBeanServerC } } - diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxSslSocketFactory.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxSslSocketFactory.java index bd20cb2e1..011d3ab17 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxSslSocketFactory.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/jmx/JmxSslSocketFactory.java @@ -1,23 +1,21 @@ package io.kafbat.ui.service.metrics.scrape.jmx; import com.google.common.base.Preconditions; -import java.io.FileInputStream; +import io.kafbat.ui.config.ClustersProperties; +import io.kafbat.ui.util.SslBundleUtil; import java.io.IOException; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyStore; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; import javax.rmi.ssl.SslRMIClientSocketFactory; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.ResourceUtils; +import org.apache.kafka.common.security.ssl.SslFactory; +import org.springframework.boot.ssl.SslBundle; /* * Purpose of this class to provide an ability to connect to different JMX endpoints using different keystores. @@ -79,18 +77,13 @@ public static boolean initialized() { private record HostAndPort(String host, int port) { } - private record Ssl(@Nullable String truststoreLocation, - @Nullable String truststorePassword, - @Nullable String keystoreLocation, - @Nullable String keystorePassword) { + private record Ssl(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { } - public static void setSslContextThreadLocal(@Nullable String truststoreLocation, - @Nullable String truststorePassword, - @Nullable String keystoreLocation, - @Nullable String keystorePassword) { - SSL_CONTEXT_THREAD_LOCAL.set( - new Ssl(truststoreLocation, truststorePassword, keystoreLocation, keystorePassword)); + public static void setSslContextThreadLocal(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { + SSL_CONTEXT_THREAD_LOCAL.set(new Ssl(truststoreConfig, keystoreConfig)); } // should be called when (host:port) -> factory cache should be invalidated (ex. on app config reload) @@ -118,33 +111,16 @@ public JmxSslSocketFactory() { @SneakyThrows private javax.net.ssl.SSLSocketFactory createFactoryFromThreadLocalCtx() { Ssl ssl = Preconditions.checkNotNull(SSL_CONTEXT_THREAD_LOCAL.get()); - - var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - if (ssl.truststoreLocation() != null && ssl.truststorePassword() != null) { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load( - new FileInputStream((ResourceUtils.getFile(ssl.truststoreLocation()))), - ssl.truststorePassword().toCharArray() - ); - trustManagerFactory.init(trustStore); - } - - var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - if (ssl.keystoreLocation() != null && ssl.keystorePassword() != null) { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load( - new FileInputStream(ResourceUtils.getFile(ssl.keystoreLocation())), - ssl.keystorePassword().toCharArray() - ); - keyManagerFactory.init(keyStore, ssl.keystorePassword().toCharArray()); + SslBundle bundle = SslBundleUtil.loadBundle(ssl.truststoreConfig(), ssl.keystoreConfig); + + SSLContext ctx; + if (bundle != null) { + ctx = bundle.createSslContext(); + } else { + ctx = SSLContext.getInstance("TLS"); + ctx.init(null, null, null); } - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init( - keyManagerFactory.getKeyManagers(), - trustManagerFactory.getTrustManagers(), - null - ); return ctx.getSocketFactory(); } diff --git a/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java b/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java index 384888aa1..66d8743e4 100644 --- a/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java +++ b/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java @@ -1,9 +1,13 @@ package io.kafbat.ui.util; +import io.kafbat.ui.api.model.SecurityProtocol; import io.kafbat.ui.config.ClustersProperties; import java.util.Properties; import javax.annotation.Nullable; +import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.common.config.SslConfigs; +import org.springframework.boot.autoconfigure.kafka.SslBundleSslEngineFactory; +import org.springframework.boot.ssl.SslBundle; public final class KafkaClientSslPropertiesUtil { @@ -11,24 +15,23 @@ private KafkaClientSslPropertiesUtil() { } public static void addKafkaSslProperties(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig, + @Nullable SecurityProtocol securityProtocol, Properties sink) { - if (truststoreConfig == null) { - return; + if (securityProtocol != null) { + sink.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol.name()); } - if (!truststoreConfig.isVerifySsl()) { + if (truststoreConfig != null && !truststoreConfig.isVerifySsl()) { sink.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); } - if (truststoreConfig.getTruststoreLocation() == null) { + SslBundle bundle = SslBundleUtil.loadBundle(truststoreConfig, keystoreConfig); + if (bundle == null) { return; } - sink.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreConfig.getTruststoreLocation()); - - if (truststoreConfig.getTruststorePassword() != null) { - sink.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststoreConfig.getTruststorePassword()); - } - + sink.put(SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG, SslBundleSslEngineFactory.class); + sink.put(SslBundle.class.getName(), bundle); } } diff --git a/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java b/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java index 1871dbcc1..c51316578 100644 --- a/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java +++ b/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java @@ -2,24 +2,23 @@ import static io.kafbat.ui.config.ClustersProperties.TruststoreConfig; +import io.kafbat.ui.api.model.SecurityProtocol; +import io.kafbat.ui.config.ClustersProperties.KeystoreConfig; import io.kafbat.ui.connect.api.KafkaConnectClientApi; import io.kafbat.ui.model.ApplicationPropertyValidationDTO; import io.kafbat.ui.prometheus.api.PrometheusClientApi; import io.kafbat.ui.service.ReactiveAdminClient; import io.kafbat.ui.service.ksql.KsqlApiClient; import io.kafbat.ui.sr.api.KafkaSrClientApi; -import java.io.FileInputStream; -import java.security.KeyStore; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.function.Supplier; import javax.annotation.Nullable; -import javax.net.ssl.TrustManagerFactory; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; -import org.springframework.util.ResourceUtils; +import org.springframework.boot.ssl.SslBundle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,28 +43,25 @@ private static Mono invalid(Throwable th) { /** * Returns error msg, if any. */ - public static Optional validateTruststore(TruststoreConfig truststoreConfig) { - if (truststoreConfig.getTruststoreLocation() != null && truststoreConfig.getTruststorePassword() != null) { - try (FileInputStream fileInputStream = new FileInputStream( - (ResourceUtils.getFile(truststoreConfig.getTruststoreLocation())))) { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(fileInputStream, truststoreConfig.getTruststorePassword().toCharArray()); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm() - ); - trustManagerFactory.init(trustStore); - } catch (Exception e) { - return Optional.of(e.getMessage()); - } + public static Optional validateSslBundle(TruststoreConfig truststoreConfig, KeystoreConfig keystoreConfig) { + try { + SslBundle bundle = SslBundleUtil.loadBundle(truststoreConfig, keystoreConfig); + bundle.createSslContext().createSSLEngine(); + } catch (Exception e) { + return Optional.of(e.getMessage()); } return Optional.empty(); } - public static Mono validateClusterConnection(String bootstrapServers, - Properties clusterProps, - @Nullable TruststoreConfig ssl) { + public static Mono validateClusterConnection( + String bootstrapServers, + Properties clusterProps, + @Nullable TruststoreConfig ssl, + @Nullable KeystoreConfig kafkaSsl, + @Nullable SecurityProtocol securityProtocol + ) { Properties properties = new Properties(); - KafkaClientSslPropertiesUtil.addKafkaSslProperties(ssl, properties); + KafkaClientSslPropertiesUtil.addKafkaSslProperties(ssl, kafkaSsl, securityProtocol, properties); properties.putAll(clusterProps); properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); // editing properties to make validation faster diff --git a/api/src/main/java/io/kafbat/ui/util/SslBundleUtil.java b/api/src/main/java/io/kafbat/ui/util/SslBundleUtil.java new file mode 100644 index 000000000..e0acd821f --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/util/SslBundleUtil.java @@ -0,0 +1,99 @@ +package io.kafbat.ui.util; + +import io.kafbat.ui.config.ClustersProperties; +import io.kafbat.ui.config.ClustersProperties.StoreType; +import io.kafbat.ui.exception.ValidationException; +import javax.annotation.Nullable; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStore; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; + +public class SslBundleUtil { + public static SslBundle loadBundle(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { + if (truststoreConfig == null && keystoreConfig == null) { + return null; + } + + StoreType type = null; + if (truststoreConfig != null && truststoreConfig.getTruststoreType() != null) { + type = truststoreConfig.getTruststoreType(); + } + + if (keystoreConfig != null && keystoreConfig.getKeystoreType() != null) { + if (type != null && type != keystoreConfig.getKeystoreType()) { + throw new ValidationException("Truststore/keystore types must match"); + } + + type = keystoreConfig.getKeystoreType(); + } + + if (type == null) { + type = StoreType.JKS; + } + + return switch (type) { + case JKS, PKCS12 -> loadJksBundle(truststoreConfig, keystoreConfig, type); + case PEM -> loadPemBundle(truststoreConfig, keystoreConfig); + }; + } + + public static SslBundle loadPemBundle(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { + PemSslStore keyStore = null; + PemSslStore trustStore = null; + if (keystoreConfig != null && keystoreConfig.getKeystoreLocation() != null) { + keyStore = PemSslStore.load( + new PemSslStoreDetails( + null, + null, + null, + keystoreConfig.getKeystoreCertificate(), + keystoreConfig.getKeystoreLocation(), + keystoreConfig.getKeystorePassword() + ) + ); + } + + if (truststoreConfig != null && truststoreConfig.getTruststoreLocation() != null) { + trustStore = PemSslStore.load( + new PemSslStoreDetails( + null, + truststoreConfig.getTruststoreLocation(), + null + ) + ); + } + + return SslBundle.of(new PemSslStoreBundle(keyStore, trustStore)); + } + + public static SslBundle loadJksBundle(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + @Nullable ClustersProperties.KeystoreConfig keystoreConfig, + StoreType type) { + JksSslStoreDetails keyStore = null; + JksSslStoreDetails trustStore = null; + if (keystoreConfig != null && keystoreConfig.getKeystoreLocation() != null) { + keyStore = new JksSslStoreDetails( + type.name(), + null, + keystoreConfig.getKeystoreLocation(), + keystoreConfig.getKeystorePassword() + ); + } + + if (truststoreConfig != null && truststoreConfig.getTruststoreLocation() != null) { + trustStore = new JksSslStoreDetails( + type.name(), + null, + truststoreConfig.getTruststoreLocation(), + truststoreConfig.getTruststorePassword() + ); + } + + return SslBundle.of(new JksSslStoreBundle(keyStore, trustStore)); + } +} diff --git a/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java b/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java index 170530be1..08353d76c 100644 --- a/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java +++ b/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java @@ -5,18 +5,25 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.exception.ValidationException; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; +import io.netty.handler.ssl.JdkSslContext; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import java.io.FileInputStream; import java.security.KeyStore; import java.time.Duration; +import java.util.Arrays; import java.util.function.Consumer; import javax.annotation.Nullable; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import lombok.SneakyThrows; import org.openapitools.jackson.nullable.JsonNullableModule; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.ClientCodecConfigurer; @@ -51,53 +58,23 @@ public WebClientConfigurator configureSsl(@Nullable ClustersProperties.Truststor return configureNoSsl(); } - return configureSsl( - keystoreConfig != null ? keystoreConfig.getKeystoreLocation() : null, - keystoreConfig != null ? keystoreConfig.getKeystorePassword() : null, - truststoreConfig != null ? truststoreConfig.getTruststoreLocation() : null, - truststoreConfig != null ? truststoreConfig.getTruststorePassword() : null - ); - } - - @SneakyThrows - private WebClientConfigurator configureSsl( - @Nullable String keystoreLocation, - @Nullable String keystorePassword, - @Nullable String truststoreLocation, - @Nullable String truststorePassword) { - if (truststoreLocation == null && keystoreLocation == null) { - return this; - } - - SslContextBuilder contextBuilder = SslContextBuilder.forClient(); - if (truststoreLocation != null && truststorePassword != null) { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load( - new FileInputStream((ResourceUtils.getFile(truststoreLocation))), - truststorePassword.toCharArray() - ); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm() - ); - trustManagerFactory.init(trustStore); - contextBuilder.trustManager(trustManagerFactory); - } - - // Prepare keystore only if we got a keystore - if (keystoreLocation != null && keystorePassword != null) { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load( - new FileInputStream(ResourceUtils.getFile(keystoreLocation)), - keystorePassword.toCharArray() - ); - - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, keystorePassword.toCharArray()); - contextBuilder.keyManager(keyManagerFactory); + SslBundle bundle = SslBundleUtil.loadBundle(truststoreConfig, keystoreConfig); + if (bundle == null) { + return configureNoSsl(); } - // Create webclient - SslContext context = contextBuilder.build(); + SSLContext sslContext = bundle.createSslContext(); + String[] ciphers = sslContext.createSSLEngine().getSupportedCipherSuites(); + JdkSslContext context = new JdkSslContext( + sslContext, + true, + Arrays.asList(ciphers), + IdentityCipherSuiteFilter.INSTANCE, + null, + ClientAuth.NONE, + null, + false + ); httpClient = httpClient.secure(t -> t.sslContext(context)); return this; diff --git a/contract-typespec/api/config.tsp b/contract-typespec/api/config.tsp index 39647a3b1..2f991e0ea 100644 --- a/contract-typespec/api/config.tsp +++ b/contract-typespec/api/config.tsp @@ -64,6 +64,13 @@ model ApplicationInfo { }; } +model KeystoreConfig { + keystoreType?: StoreType; + keystoreCertificate?: string; + keystoreLocation?: string; + keystorePassword?: string; +} + model ApplicationConfig { properties: { auth?: { @@ -143,25 +150,22 @@ model ApplicationConfig { clusters?: { name?: string; bootstrapServers?: string; + securityProtocol?: string; ssl?: { + truststoreType?: StoreType; truststoreLocation?: string; truststorePassword?: string; verifySsl?: boolean = true; }; + kafkaSsl?: KeystoreConfig; schemaRegistry?: string; schemaRegistryAuth?: { username?: string; password?: string; }; - schemaRegistrySsl?: { - keystoreLocation?: string; - keystorePassword?: string; - }; + schemaRegistrySsl?: KeystoreConfig; ksqldbServer?: string; - ksqldbServerSsl?: { - keystoreLocation?: string; - keystorePassword?: string; - }; + ksqldbServerSsl?: KeystoreConfig; ksqldbServerAuth?: { username?: string; password?: string; @@ -171,6 +175,8 @@ model ApplicationConfig { address?: string; username?: string; password?: string; + keystoreType?: StoreType; + keystoreCertificate?: string; keystoreLocation?: string; keystorePassword?: string; }[]; @@ -180,6 +186,8 @@ model ApplicationConfig { ssl?: boolean; username?: string; password?: string; + keystoreType?: StoreType; + keystoreCertificate?: string; keystoreLocation?: string; keystorePassword?: string; prometheusExpose?: boolean; @@ -300,6 +308,19 @@ enum ResourceType { CLIENT_QUOTAS, } +enum StoreType { + JKS, + PKCS12, + PEM, +} + +enum SecurityProtocol { + PLAINTEXT, + SASL_PLAINTEXT, + SASL_SSL, + SSL, +} + model ClusterMetricsStoreConfig { prometheus?: PrometheusStorage; } diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index dd0dce0d8..56e7760c0 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -4455,6 +4455,30 @@ components: prometheusStorage: $ref: '#/components/schemas/ApplicationPropertyValidation' + StoreType: + type: string + enum: + - JKS + - PKCS12 + - PEM + SecurityProtocol: + type: string + enum: + - PLAINTEXT + - SASL_PLAINTEXT + - SASL_SSL + - SSL + KeystoreConfig: + type: object + properties: + keystoreType: + $ref: '#/components/schemas/StoreType' + keystoreCertificate: + type: string + keystoreLocation: + type: string + keystorePassword: + type: string ApplicationConfig: type: object properties: @@ -4637,9 +4661,13 @@ components: type: string bootstrapServers: type: string + securityProtocol: + $ref: '#/components/schemas/SecurityProtocol' ssl: type: object properties: + truststoreType: + $ref: '#/components/schemas/StoreType' truststoreLocation: type: string truststorePassword: @@ -4648,6 +4676,8 @@ components: type: boolean description: Skip SSL verification for the host. default: true + kafkaSsl: + $ref: '#/components/schemas/KeystoreConfig' schemaRegistry: type: string schemaRegistryAuth: @@ -4658,21 +4688,11 @@ components: password: type: string schemaRegistrySsl: - type: object - properties: - keystoreLocation: - type: string - keystorePassword: - type: string + $ref: '#/components/schemas/KeystoreConfig' ksqldbServer: type: string ksqldbServerSsl: - type: object - properties: - keystoreLocation: - type: string - keystorePassword: - type: string + $ref: '#/components/schemas/KeystoreConfig' ksqldbServerAuth: type: object properties: @@ -4693,6 +4713,10 @@ components: type: string password: type: string + keystoreType: + $ref: '#/components/schemas/StoreType' + keystoreCertificate: + type: string keystoreLocation: type: string keystorePassword: @@ -4712,6 +4736,10 @@ components: type: string password: type: string + keystoreType: + $ref: '#/components/schemas/StoreType' + keystoreCertificate: + type: string keystoreLocation: type: string keystorePassword: diff --git a/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts b/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts index cf8e33326..d1e6fa2df 100644 --- a/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts +++ b/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts @@ -1,6 +1,6 @@ import { ApplicationConfigPropertiesKafkaClusters, - ApplicationConfigPropertiesKafkaSchemaRegistrySsl, + KeystoreConfig, } from 'generated-sources'; import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types'; @@ -12,9 +12,7 @@ const parseBootstrapServers = (bootstrapServers?: string) => return { host, port }; }); -const parseKeystore = ( - keystore?: ApplicationConfigPropertiesKafkaSchemaRegistrySsl -) => { +const parseKeystore = (keystore?: KeystoreConfig) => { if (!keystore) return undefined; const { keystoreLocation, keystorePassword } = keystore; return {