CLI options to disable encryption and authentication to external Infinispan

Closes #28750

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
This commit is contained in:
Pedro Ruivo 2024-04-15 18:33:03 +01:00 committed by Alexander Schwartz
parent 6977d58d27
commit 3de5357091
8 changed files with 114 additions and 128 deletions

View file

@ -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.

View file

@ -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

View file

@ -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<Stack> 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<Boolean> 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();
}

View file

@ -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<CacheManagerFactory> 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);

View file

@ -137,13 +137,7 @@ public final class Configuration {
}
public static Optional<Boolean> getOptionalBooleanKcValue(String propertyName) {
Optional<String> 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<Boolean> getOptionalBooleanValue(String name) {

View file

@ -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<DefaultCacheManager> cacheManagerFuture;
private ExecutorService executor;
private boolean initialized;
private final String config;
private final CompletableFuture<DefaultCacheManager> 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);
}
public void shutdown() {
logger.debug("Shutdown embedded cache manager");
cacheManagerFuture.thenAccept(CacheManagerFactory::close);
}
private static <T> T join(Future<T> future) {
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();
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 static void close(Lifecycle lifecycle) {
if (lifecycle != null) {
lifecycle.stop();
}
}
private ExecutorService createThreadPool() {
return Executors.newSingleThreadExecutor(r -> new Thread(r, "keycloak-cache-init"));
}
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,26 +247,29 @@ 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);
});
}
if (isRemoteTLSEnabled()) {
storeBuilder.remoteSecurity()
.ssl()
.enable()
.sslContext(sslContext)
.sniHostName(cacheRemoteHost);
}
private static boolean booleanProperty(String propertyName) {
return Configuration.getOptionalKcValue(propertyName).map(Boolean::parseBoolean).orElse(Boolean.FALSE);
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 String requiredStringProperty(String propertyName) {

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -28,7 +27,7 @@ import io.quarkus.arc.Arc;
public final class QuarkusCacheManagerProvider implements ManagedCacheManagerProvider {
@Override
public <C> C getCacheManager(Config.Scope config) {
return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreate();
public <C> C getEmbeddedCacheManager(Config.Scope config) {
return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager();
}
}

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ManagedCacheManagerProvider {
<C> C getCacheManager(Config.Scope config);
<C> C getEmbeddedCacheManager(Config.Scope config);
}