diff --git a/docs/guides/server/caching.adoc b/docs/guides/server/caching.adoc index 0d0d7f5ad1..8a697cec56 100644 --- a/docs/guides/server/caching.adoc +++ b/docs/guides/server/caching.adoc @@ -151,6 +151,16 @@ The configuration file is relative to the `conf/` directory. For configuration of {project_name} server for high availability and multi-node clustered setup there was introduced following CLI options `cache-remote-host`, `cache-remote-port`, `cache-remote-username` and `cache-remote-password` simplifying configuration within the XML file. Once any of declared CLI parameters are present, it is expected there is no configuration related to remote store present in the XML file. +==== Connecting to an insecure Infinispan server + +WARNING: Disabling security is not recommended in production! + +In development or test environment, it is easier to start an unsecured Infinispan server. +For these use case, the CLI options `cache-remote-tls-enabled` disables the encryption (SSL) between {project_name} and Infinispan. +{project_name} will fail to start if the Infinispan server is configured to accept only encrypted connections. + +The CLI options `cache-remote-username` and `cache-remote-password` are optional and, if not set, {project_name} will connect to the Infinispan server without presenting any credentials. +If the Infinispan server has authentication enabled, {project_name} will fail to start. == Transport stacks Transport stacks ensure that distributed cache nodes in a cluster communicate in a reliable fashion. diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 6e52787f3f..25b7340aa9 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -186,7 +186,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon throw new RuntimeException("Multiple " + org.keycloak.cluster.ManagedCacheManagerProvider.class + " providers found."); } - managedCacheManager = provider.getCacheManager(config); + managedCacheManager = provider.getEmbeddedCacheManager(config); } // store it in a locale variable first, so it is not visible to the outside, yet diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java index e65f96c3cc..69de261894 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java @@ -21,6 +21,7 @@ public class CachingOptions { public static final String CACHE_REMOTE_PORT_PROPERTY = CACHE_REMOTE_PREFIX + "-port"; public static final String CACHE_REMOTE_USERNAME_PROPERTY = CACHE_REMOTE_PREFIX + "-username"; public static final String CACHE_REMOTE_PASSWORD_PROPERTY = CACHE_REMOTE_PREFIX + "-password"; + public static final String CACHE_REMOTE_TLS_ENABLED_PROPERTY = CACHE_REMOTE_PREFIX + "-tls-enabled"; private static final String CACHE_METRICS_PREFIX = "cache-metrics"; public static final String CACHE_METRICS_HISTOGRAMS_ENABLED_PROPERTY = CACHE_METRICS_PREFIX + "-histograms-enabled"; @@ -44,7 +45,7 @@ public class CachingOptions { kubernetes, ec2, azure, - google; + google } public static final Option CACHE_STACK = new OptionBuilder<>("cache-stack", Stack.class) @@ -126,4 +127,9 @@ public class CachingOptions { .description("Enable histograms for metrics for the embedded caches.") .build(); + public static final Option CACHE_REMOTE_TLS_ENABLED = new OptionBuilder<>(CACHE_REMOTE_TLS_ENABLED_PROPERTY, Boolean.class) + .category(OptionCategory.CACHE) + .description("Enable SSL support to communication with a secure remote Infinispan server. It is not recommended to disable in production!") + .defaultValue(Boolean.TRUE) + .build(); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index 9d1cdceb8d..09051c92cc 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -36,37 +36,32 @@ import io.quarkus.agroal.DataSource; import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationRuntimeInitListener; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import liquibase.Scope; - +import liquibase.servicelocator.ServiceLocator; import org.hibernate.cfg.AvailableSettings; import org.infinispan.commons.util.FileLookupFactory; -import org.infinispan.manager.DefaultCacheManager; - import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.crypto.CryptoProvider; import org.keycloak.common.crypto.FipsMode; -import org.keycloak.config.MetricsOptions; import org.keycloak.config.TruststoreOptions; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import org.keycloak.quarkus.runtime.storage.database.liquibase.FastServiceLocator; -import org.keycloak.provider.Provider; -import org.keycloak.provider.ProviderFactory; -import org.keycloak.provider.Spi; import org.keycloak.quarkus.runtime.storage.legacy.infinispan.CacheManagerFactory; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.theme.ClasspathThemeProviderFactory; import org.keycloak.truststore.TruststoreBuilder; - -import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.ShutdownContext; -import io.quarkus.runtime.annotations.Recorder; -import liquibase.servicelocator.ServiceLocator; import org.keycloak.userprofile.DeclarativeUserProfileProviderFactory; import static org.keycloak.quarkus.runtime.configuration.Configuration.getKcConfigValue; @@ -123,17 +118,8 @@ public class KeycloakRecorder { public RuntimeValue createCacheInitializer(ShutdownContext shutdownContext) { try { - boolean isMetricsEnabled = Configuration.isTrue(MetricsOptions.METRICS_ENABLED); - CacheManagerFactory cacheManagerFactory = new CacheManagerFactory(getInfinispanConfigFile(), isMetricsEnabled); - - shutdownContext.addShutdownTask(() -> { - DefaultCacheManager cacheManager = cacheManagerFactory.getOrCreate(); - - if (cacheManager != null) { - cacheManager.stop(); - } - }); - + CacheManagerFactory cacheManagerFactory = new CacheManagerFactory(getInfinispanConfigFile()); + shutdownContext.addShutdownTask(cacheManagerFactory::shutdown); return new RuntimeValue<>(cacheManagerFactory); } catch (Exception e) { throw new RuntimeException(e); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java index 468d40631f..b1b2ae28cf 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java @@ -137,13 +137,7 @@ public final class Configuration { } public static Optional getOptionalBooleanKcValue(String propertyName) { - Optional value = getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName)); - - if (value.isPresent()) { - return value.map(Boolean::parseBoolean); - } - - return Optional.empty(); + return getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName)).map(Boolean::parseBoolean); } public static Optional getOptionalBooleanValue(String name) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java index a6be9b136d..edc6e51e37 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/CacheManagerFactory.java @@ -19,13 +19,15 @@ package org.keycloak.quarkus.runtime.storage.legacy.infinispan; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import io.micrometer.core.instrument.Metrics; import org.infinispan.client.hotrod.impl.ConfigurationProperties; +import org.infinispan.commons.api.Lifecycle; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.PersistenceConfigurationBuilder; import org.infinispan.configuration.global.GlobalConfiguration; @@ -43,16 +45,16 @@ import org.jgroups.protocols.UDP; import org.jgroups.util.TLS; import org.jgroups.util.TLSClientAuth; import org.keycloak.common.Profile; +import org.keycloak.config.CachingOptions; +import org.keycloak.config.MetricsOptions; import org.keycloak.quarkus.runtime.configuration.Configuration; import javax.net.ssl.SSLContext; -import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY; -import static org.keycloak.config.CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_HOST_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PORT_PROPERTY; @@ -68,44 +70,41 @@ public class CacheManagerFactory { private static final Logger logger = Logger.getLogger(CacheManagerFactory.class); - private String config; - private final boolean metricsEnabled; - private DefaultCacheManager cacheManager; - private Future cacheManagerFuture; - private ExecutorService executor; - private boolean initialized; + private final String config; + private final CompletableFuture cacheManagerFuture; - public CacheManagerFactory(String config, boolean metricsEnabled) { + public CacheManagerFactory(String config) { this.config = config; - this.metricsEnabled = metricsEnabled; - this.executor = createThreadPool(); - this.cacheManagerFuture = executor.submit(this::startCacheManager); + this.cacheManagerFuture = CompletableFuture.supplyAsync(this::startEmbeddedCacheManager); } - public DefaultCacheManager getOrCreate() { - if (cacheManager == null) { - if (initialized) { - return null; - } + public DefaultCacheManager getOrCreateEmbeddedCacheManager() { + return join(cacheManagerFuture); + } - try { - // for now, we don't have any explicit property for setting the cache start timeout - return cacheManager = cacheManagerFuture.get(getStartTimeout(), TimeUnit.SECONDS); - } catch (Exception e) { - throw new RuntimeException("Failed to start caches", e); - } finally { - shutdownThreadPool(); - } + public void shutdown() { + logger.debug("Shutdown embedded cache manager"); + cacheManagerFuture.thenAccept(CacheManagerFactory::close); + } + + private static T join(Future future) { + try { + return future.get(getStartTimeout(), TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } catch (ExecutionException | TimeoutException e) { + throw new RuntimeException("Failed to start embedded or remote cache manager", e); } - - return cacheManager; } - private ExecutorService createThreadPool() { - return Executors.newSingleThreadExecutor(r -> new Thread(r, "keycloak-cache-init")); + private static void close(Lifecycle lifecycle) { + if (lifecycle != null) { + lifecycle.stop(); + } } - private DefaultCacheManager startCacheManager() { + private DefaultCacheManager startEmbeddedCacheManager() { ConfigurationBuilderHolder builder = new ParserRegistry().parse(config); if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) { @@ -124,12 +123,12 @@ public class CacheManagerFactory { } }); - if (metricsEnabled) { + if (Configuration.isTrue(MetricsOptions.METRICS_ENABLED)) { builder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class); builder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); builder.getGlobalConfigurationBuilder().cacheContainer().statistics(true); builder.getGlobalConfigurationBuilder().metrics().namesAsTags(true); - if (booleanProperty(CACHE_METRICS_HISTOGRAMS_ENABLED_PROPERTY)) { + if (Configuration.isTrue(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)) { builder.getGlobalConfigurationBuilder().metrics().histograms(true); } builder.getNamedConfigurationBuilders().forEach((s, configurationBuilder) -> configurationBuilder.statistics().enabled(true)); @@ -143,36 +142,35 @@ public class CacheManagerFactory { return new DefaultCacheManager(builder, isStartEagerly()); } - private boolean isStartEagerly() { + private static boolean isRemoteTLSEnabled() { + return Configuration.isTrue(CachingOptions.CACHE_REMOTE_TLS_ENABLED); + } + + private static boolean isRemoteAuthenticationEnabled() { + return Configuration.getOptionalValue(CACHE_REMOTE_USERNAME_PROPERTY).isPresent() || + Configuration.getOptionalValue(CACHE_REMOTE_PASSWORD_PROPERTY).isEmpty(); + } + + private static SSLContext createSSLContext() { + try { + // uses the default Java Runtime TrustStore, or the one generated by Keycloak (see org.keycloak.truststore.TruststoreBuilder) + var sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + private static boolean isStartEagerly() { // eagerly starts caches by default return Boolean.parseBoolean(System.getProperty("kc.cache-ispn-start-eagerly", Boolean.TRUE.toString())); } - private Integer getStartTimeout() { + private static int getStartTimeout() { return Integer.getInteger("kc.cache-ispn-start-timeout", 120); } - private void shutdownThreadPool() { - if (executor != null) { - executor.shutdown(); - try { - if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { - executor.shutdownNow(); - if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { - Logger.getLogger(CacheManagerFactory.class).warn("Cache init thread pool not terminated"); - } - } - } catch (Exception cause) { - executor.shutdownNow(); - } finally { - executor = null; - cacheManagerFuture = null; - config = null; - initialized = true; - } - } - } - private void configureTransportStack(ConfigurationBuilderHolder builder) { String transportStack = Configuration.getRawValue("kc.cache-stack"); @@ -181,7 +179,7 @@ public class CacheManagerFactory { transportConfig.defaultTransport().stack(transportStack); } - if (booleanProperty(CACHE_EMBEDDED_MTLS_ENABLED_PROPERTY)) { + if (Configuration.isTrue(CachingOptions.CACHE_REMOTE_TLS_ENABLED)) { validateTlsAvailable(transportConfig.build()); var tls = new TLS() .enabled(true) @@ -218,25 +216,14 @@ public class CacheManagerFactory { private void configureRemoteStores(ConfigurationBuilderHolder builder) { //if one of remote store command line parameters is defined, some other are required, otherwise assume it'd configured via xml only - if (Configuration.getOptionalKcValue(CACHE_REMOTE_HOST_PROPERTY).isPresent() || - Configuration.getOptionalKcValue(CACHE_REMOTE_USERNAME_PROPERTY).isPresent() || - Configuration.getOptionalKcValue(CACHE_REMOTE_PASSWORD_PROPERTY).isPresent()) { + if (Configuration.getOptionalKcValue(CACHE_REMOTE_HOST_PROPERTY).isPresent()) { String cacheRemoteHost = requiredStringProperty(CACHE_REMOTE_HOST_PROPERTY); Integer cacheRemotePort = Configuration.getOptionalKcValue(CACHE_REMOTE_PORT_PROPERTY) .map(Integer::parseInt) .orElse(ConfigurationProperties.DEFAULT_HOTROD_PORT); - String cacheRemoteUsername = requiredStringProperty(CACHE_REMOTE_USERNAME_PROPERTY); - String cacheRemotePassword = requiredStringProperty(CACHE_REMOTE_PASSWORD_PROPERTY); - SSLContext sslContext; - try { - // uses the default Java Runtime TrustStore, or the one generated by Keycloak (see org.keycloak.truststore.TruststoreBuilder) - sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new RuntimeException(e); - } + SSLContext sslContext = createSSLContext(); DISTRIBUTED_REPLICATED_CACHE_NAMES.forEach(cacheName -> { if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS_NO_CACHE) && @@ -251,7 +238,8 @@ public class CacheManagerFactory { throw new RuntimeException(String.format("Remote store for cache '%s' is already configured via CLI parameters. It should not be present in the XML file.", cacheName)); } - persistenceCB.addStore(RemoteStoreConfigurationBuilder.class) + var storeBuilder = persistenceCB.addStore(RemoteStoreConfigurationBuilder.class); + storeBuilder .rawValues(true) .shared(true) .segmented(false) @@ -259,28 +247,31 @@ public class CacheManagerFactory { .connectionPool() .maxActive(16) .exhaustedAction(ExhaustedAction.CREATE_NEW) - .remoteSecurity() - .ssl() - .enable() - .sslContext(sslContext) - .sniHostName(cacheRemoteHost) - .authentication() - .enable() - .username(cacheRemoteUsername) - .password(cacheRemotePassword) - .realm("default") - .saslMechanism(SCRAM_SHA_512) .addServer() - .host(cacheRemoteHost) - .port(cacheRemotePort); + .host(cacheRemoteHost) + .port(cacheRemotePort); + + if (isRemoteTLSEnabled()) { + storeBuilder.remoteSecurity() + .ssl() + .enable() + .sslContext(sslContext) + .sniHostName(cacheRemoteHost); + } + + if (isRemoteAuthenticationEnabled()) { + storeBuilder.remoteSecurity() + .authentication() + .enable() + .username(requiredStringProperty(CACHE_REMOTE_USERNAME_PROPERTY)) + .password(requiredStringProperty(CACHE_REMOTE_PASSWORD_PROPERTY)) + .realm("default") + .saslMechanism(SCRAM_SHA_512); + } }); } } - private static boolean booleanProperty(String propertyName) { - return Configuration.getOptionalKcValue(propertyName).map(Boolean::parseBoolean).orElse(Boolean.FALSE); - } - private static String requiredStringProperty(String propertyName) { return Configuration.getOptionalKcValue(propertyName).orElseThrow(() -> new RuntimeException("Property " + propertyName + " required but not specified")); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/QuarkusCacheManagerProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/QuarkusCacheManagerProvider.java index a5f5a546e2..ba54f55a26 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/QuarkusCacheManagerProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/legacy/infinispan/QuarkusCacheManagerProvider.java @@ -17,10 +17,9 @@ package org.keycloak.quarkus.runtime.storage.legacy.infinispan; -import org.keycloak.cluster.ManagedCacheManagerProvider; -import org.keycloak.Config; - import io.quarkus.arc.Arc; +import org.keycloak.Config; +import org.keycloak.cluster.ManagedCacheManagerProvider; /** * @author Pedro Igor @@ -28,7 +27,7 @@ import io.quarkus.arc.Arc; public final class QuarkusCacheManagerProvider implements ManagedCacheManagerProvider { @Override - public C getCacheManager(Config.Scope config) { - return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreate(); + public C getEmbeddedCacheManager(Config.Scope config) { + return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(); } } diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java index 2f32bf3476..61a35512ba 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java @@ -20,11 +20,11 @@ package org.keycloak.cluster; import org.keycloak.Config; /** - * A Service Provider Interface (SPI) that allows to plug-in a cache manager instance. + * A Service Provider Interface (SPI) that allows to plug-in an embedded cache manager instance. * * @author Pedro Igor */ public interface ManagedCacheManagerProvider { - C getCacheManager(Config.Scope config); + C getEmbeddedCacheManager(Config.Scope config); }