Create CA certificate for JGroups encryption

Closes #36750

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
Signed-off-by: Pedro Ruivo <pruivo@users.noreply.github.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
Pedro Ruivo 2025-02-13 10:32:43 +00:00 committed by GitHub
parent 8a03661ba0
commit 70e2a28ff9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1378 additions and 264 deletions

View File

@ -814,6 +814,41 @@ jobs:
with:
job-id: clustering-integration-tests
clustering-integration-tests-mtls:
name: Clustering IT (mTLS)
needs: build
runs-on: ubuntu-latest
timeout-minutes: 35
env:
MAVEN_OPTS: -Xmx1536m
steps:
- uses: actions/checkout@v4
- id: integration-test-setup
name: Integration test setup
uses: ./.github/actions/integration-test-setup
- name: Run cluster tests with mtls
run: |
./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-cluster-quarkus,db-postgres "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -Dsession.cache.owners=2 -Dtest=RealmInvalidationClusterTest -Dauth.server.jgroups.mtls=true -pl testsuite/integration-arquillian/tests/base
- name: Upload JVM Heapdumps
if: always()
uses: ./.github/actions/upload-heapdumps
- uses: ./.github/actions/upload-flaky-tests
name: Upload flaky tests
env:
GH_TOKEN: ${{ github.token }}
with:
job-name: Clustering IT (mTLS)
- name: Surefire reports
if: always()
uses: ./.github/actions/archive-surefire-reports
with:
job-id: clustering-integration-tests-mtls
fips-unit-tests:
name: FIPS UT
runs-on: ubuntu-latest

View File

@ -304,6 +304,18 @@ It requires a keystore with the certificate to use: `cache-embedded-mtls-key-sto
The truststore contains the valid certificates to accept connection from, and it can be configured with `cache-embedded-mtls-trust-store-file` (path to the truststore), and `cache-embedded-mtls-trust-store-password` (password to decrypt it).
To restrict unauthorized access, use a self-signed certificate for each {project_name} deployment.
[NOTE]
====
**Zero Configuration Encryption**
{project_name} offers a zero-configuration approach to encrypting network communication between nodes.
This feature automatically generates self-signed certificates, eliminating the need for manual certificate creation and management.
The generated certificate and associated keys are stored within the database of each {project_name} instance.
To enable zero-configuration TLS encryption, set the `cache-embedded-mtls-enabled` option to true.
No other `cache-embedded-mtls-*` must be set to enable the zero-configuration mode.
====
For JGroups stacks with `UDP` or `TCP_NIO2`, see the http://jgroups.org/manual5/index.html#ENCRYPT[JGroups Encryption documentation] on how to set up the protocol stack.
For more information about securing cache communication, see the {infinispan_embedding_docs}#secure-cluster-transport[Encrypting cluster transport] documentation.

View File

@ -0,0 +1,90 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.configuration.jpa;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import org.keycloak.storage.configuration.ServerConfigStorageProvider;
import org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity;
/**
* A {@link ServerConfigStorageProvider} that stores its data in the database, using the {@link EntityManager}.
*/
public class JpaServerConfigStorageProvider implements ServerConfigStorageProvider {
private final EntityManager entityManager;
public JpaServerConfigStorageProvider(EntityManager entityManager) {
this.entityManager = Objects.requireNonNull(entityManager);
}
@Override
public Optional<String> find(String key) {
return Optional.ofNullable(getEntity(key, LockModeType.READ))
.map(ServerConfigEntity::getValue);
}
@Override
public void store(String key, String value) {
var entity = getEntity(key, LockModeType.WRITE);
if (entity == null) {
entity = new ServerConfigEntity();
entity.setKey(Objects.requireNonNull(key));
entity.setValue(Objects.requireNonNull(value));
entityManager.persist(entity);
return;
}
entity.setValue(Objects.requireNonNull(value));
entityManager.merge(entity);
}
@Override
public void remove(String key) {
var entity = getEntity(key, LockModeType.WRITE);
if (entity != null) {
entityManager.remove(entity);
}
}
@Override
public String loadOrCreate(String key, Supplier<String> valueGenerator) {
var entity = getEntity(key, LockModeType.WRITE);
if (entity != null) {
return entity.getValue();
}
var value = Objects.requireNonNull(valueGenerator.get());
entity = new ServerConfigEntity();
entity.setKey(Objects.requireNonNull(key));
entity.setValue(value);
entityManager.persist(entity);
return value;
}
@Override
public void close() {
//no-op
}
private ServerConfigEntity getEntity(String key, LockModeType lockModeType) {
return entityManager.find(ServerConfigEntity.class, Objects.requireNonNull(key), lockModeType);
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.configuration.jpa;
import java.util.Set;
import jakarta.persistence.EntityManager;
import org.keycloak.Config;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.Provider;
import org.keycloak.storage.configuration.ServerConfigStorageProviderFactory;
/**
* A {@link ServerConfigStorageProviderFactory} that instantiates {@link JpaServerConfigStorageProvider}.
*/
public class JpaServerConfigStorageProviderFactory implements ServerConfigStorageProviderFactory {
@Override
public JpaServerConfigStorageProvider create(KeycloakSession session) {
return new JpaServerConfigStorageProvider(getEntityManager(session));
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "jpa";
}
@Override
public Set<Class<? extends Provider>> dependsOn() {
return Set.of(JpaConnectionProvider.class);
}
private static EntityManager getEntityManager(KeycloakSession session) {
return session.getProvider(JpaConnectionProvider.class).getEntityManager();
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.configuration.jpa.entity;
import java.util.Objects;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
/**
* A JPA entity to store the key-value configuration.
*/
@SuppressWarnings("unused")
@Table(name = "SERVER_CONFIG")
@Entity
public class ServerConfigEntity {
@Id
@Column(name = "SERVER_CONFIG_KEY")
private String key;
@Column(name = "VALUE")
private String value;
@Version
@Column(name = "VERSION")
private int version;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ServerConfigEntity that = (ServerConfigEntity) o;
return version == that.version && Objects.equals(key, that.key) && Objects.equals(value, that.value);
}
@Override
public int hashCode() {
int result = Objects.hashCode(key);
result = 31 * result + Objects.hashCode(value);
result = 31 * result + version;
return result;
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2024 Red Hat, Inc. and/or its affiliates
~ * and other contributors as indicated by the @author tags.
~ *
~ * Licensed under the Apache License, Version 2.0 (the "License");
~ * you may not use this file except in compliance with the License.
~ * You may obtain a copy of the License at
~ *
~ * http://www.apache.org/licenses/LICENSE-2.0
~ *
~ * Unless required by applicable law or agreed to in writing, software
~ * distributed under the License is distributed on an "AS IS" BASIS,
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ * See the License for the specific language governing permissions and
~ * limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="26.2.0-36750">
<createTable tableName="SERVER_CONFIG">
<column name="SERVER_CONFIG_KEY" type="VARCHAR(255)">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="VALUE" type="CLOB">
<constraints nullable="false"/>
</column>
<column name="VERSION" type="INT" defaultValueNumeric="0"/>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -85,5 +85,6 @@
<include file="META-INF/jpa-changelog-25.0.0.xml"/>
<include file="META-INF/jpa-changelog-26.0.0.xml"/>
<include file="META-INF/jpa-changelog-26.1.0.xml"/>
<include file="META-INF/jpa-changelog-26.2.0.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,18 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.storage.configuration.jpa.JpaServerConfigStorageProviderFactory

View File

@ -89,6 +89,9 @@
<class>org.keycloak.models.jpa.entities.OrganizationEntity</class>
<class>org.keycloak.models.jpa.entities.OrganizationDomainEntity</class>
<!-- Server Configuration -->
<class>org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>

View File

@ -0,0 +1,72 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.configuration;
import java.util.Optional;
import java.util.function.Supplier;
import org.keycloak.provider.Provider;
/**
* A {@link Provider} to store server configuration to be shared between the Keycloak instances.
* <p>
* This provider is a key-value store where both keys and values are {@link String}.
*/
public interface ServerConfigStorageProvider extends Provider {
/**
* Returns the value to which the specified {@code key}.
*
* @param key The {@code key} whose associated value is to be returned.
* @return The value from the specified {@code key}.
* @throws NullPointerException if the specified {@code key} is {@code null}.
*/
Optional<String> find(String key);
/**
* Stores the specified {@code value} with the specified {@code key}.
* <p>
* If the {@code key} exists, its value is updated.
*
* @param key The {@code key} with which the specified {@code value} is to be stored.
* @param value The {@code value} to be associated with the specified {@code key}.
* @throws NullPointerException if the specified {@code key} or {@code value} is {@code null}.
*/
void store(String key, String value);
/**
* Removes the {@code value} specified by the {@code key}.
*
* @param key The {@code key} whose value is to be removed.
* @throws NullPointerException if the specified {@code key} is {@code null}.
*/
void remove(String key);
/**
* Returns the value to which the specified {@code key} or, if not found, stores the value returned by the
* {@code valueGenerator}.
*
* @param key The {@code key} whose associated value is to be returned or stored.
* @param valueGenerator The {@link Supplier} to generate the value if it is not found.
* @return The {value stored by the {@code key}, or the value generated by the {@link Supplier}.
* @throws NullPointerException if the specified {@code key}, {@code valueGenerator} or {@link Supplier} return
* value is {@code null}.
*/
String loadOrCreate(String key, Supplier<String> valueGenerator);
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.configuration;
import org.keycloak.provider.ProviderFactory;
/**
* A {@link ProviderFactory} to create instances of {@link ServerConfigStorageProvider}
*/
public interface ServerConfigStorageProviderFactory extends ProviderFactory<ServerConfigStorageProvider> {
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.configuration;
import org.keycloak.provider.Spi;
/**
* The {@link Spi} implementation of {@link ServerConfigStorageProvider}.
*/
public class ServerConfigurationStorageProviderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "serverConfig";
}
@Override
public Class<ServerConfigStorageProvider> getProviderClass() {
return ServerConfigStorageProvider.class;
}
@Override
public Class<ServerConfigStorageProviderFactory> getProviderFactoryClass() {
return ServerConfigStorageProviderFactory.class;
}
}

View File

@ -17,3 +17,4 @@
org.keycloak.storage.UserStorageProviderSpi
org.keycloak.storage.federated.UserFederatedStorageProviderSpi
org.keycloak.storage.configuration.ServerConfigurationStorageProviderSpi

View File

@ -14,6 +14,7 @@ import org.keycloak.config.Option;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
@ -56,17 +57,25 @@ final class CachingPropertyMappers {
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE.withRuntimeSpecificDefault(getDefaultKeystorePathValue()))
.paramLabel("file")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD)
.paramLabel("password")
.isMasked(true)
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE.withRuntimeSpecificDefault(getDefaultTruststorePathValue()))
.paramLabel("file")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD)
.paramLabel("password")
.isMasked(true)
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE))
.build(),
fromOption(CachingOptions.CACHE_REMOTE_HOST)
.paramLabel("hostname")
@ -170,4 +179,11 @@ final class CachingPropertyMappers {
throw new PropertyException("The option '%s' is required when '%s' is set.".formatted(optionRequired.getKey(), optionSet.getKey()));
}
}
private static void checkOptionPresent(Option<String> option, Option<String> requiredOption) {
if (getOptionalKcValue(requiredOption).isPresent()) {
return;
}
throw new PropertyException("The option '%s' requires '%s' to be enabled.".formatted(option.getKey(), requiredOption.getKey()));
}
}

View File

@ -20,7 +20,6 @@ package org.keycloak.quarkus.runtime.storage.infinispan;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@ -28,15 +27,11 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import io.agroal.api.AgroalDataSource;
import io.micrometer.core.instrument.Metrics;
import io.quarkus.arc.Arc;
import jakarta.persistence.EntityManager;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
@ -48,7 +43,6 @@ import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.HashConfiguration;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.configuration.global.ShutdownHookBehavior;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.configuration.parsing.ParserRegistry;
@ -59,23 +53,13 @@ import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.infinispan.protostream.descriptors.FileDescriptor;
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.jboss.logging.Logger;
import org.jgroups.conf.ProtocolConfiguration;
import org.jgroups.protocols.JDBC_PING2;
import org.jgroups.protocols.TCP_NIO2;
import org.jgroups.protocols.UDP;
import org.jgroups.util.TLS;
import org.jgroups.util.TLSClientAuth;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultiSiteUtils;
import org.keycloak.config.CachingOptions;
import org.keycloak.config.MetricsOptions;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.connections.infinispan.InfinispanUtil;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.marshalling.KeycloakIndexSchemaUtil;
import org.keycloak.marshalling.KeycloakModelSchema;
@ -86,15 +70,10 @@ import org.keycloak.models.sessions.infinispan.query.UserSessionQueries;
import org.keycloak.models.sessions.infinispan.remote.RemoteInfinispanAuthenticationSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.remote.RemoteUserLoginFailureProviderFactory;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsConfigurator;
import javax.net.ssl.SSLContext;
import javax.sql.DataSource;
import static org.infinispan.configuration.global.TransportConfiguration.STACK;
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_REMOTE_HOST_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PORT_PROPERTY;
@ -114,26 +93,25 @@ import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.SCRA
public class CacheManagerFactory {
private static final Logger logger = Logger.getLogger(CacheManagerFactory.class);
public static final Logger logger = Logger.getLogger(CacheManagerFactory.class);
// Map with the default cache configuration if the cache is not present in the XML.
private static final Map<String, Supplier<ConfigurationBuilder>> DEFAULT_CONFIGS = Map.of(
CRL_CACHE_NAME, InfinispanUtil::getCrlCacheConfig
);
private static final Supplier<ConfigurationBuilder> TO_NULL = () -> null;
private final CompletableFuture<EmbeddedCacheManager> cacheManagerFuture;
private volatile CompletableFuture<EmbeddedCacheManager> cacheManagerFuture;
private final CompletableFuture<RemoteCacheManager> remoteCacheManagerFuture;
private final Function<EntityManager, EmbeddedCacheManager> jdbcCacheManagerFunction;
private volatile EmbeddedCacheManager cacheManager;
private final JGroupsConfigurator jGroupsConfigurator;
public CacheManagerFactory(String config) {
ConfigurationBuilderHolder builder = new ParserRegistry().parse(config);
if (!isJdbcPingRequired(builder)) {
cacheManagerFuture = CompletableFuture.supplyAsync(() -> startEmbeddedCacheManager(builder, null));
jdbcCacheManagerFunction = null;
} else {
jGroupsConfigurator = JGroupsConfigurator.create(builder);
if (jGroupsConfigurator.requiresKeycloakSession()) {
cacheManagerFuture = null;
jdbcCacheManagerFunction = em -> startEmbeddedCacheManager(builder, em);
} else {
cacheManagerFuture = CompletableFuture.supplyAsync(() -> startEmbeddedCacheManager(null));
}
if (InfinispanUtils.isRemoteInfinispan()) {
@ -145,35 +123,18 @@ public class CacheManagerFactory {
}
}
private static boolean isJdbcPingRequired(ConfigurationBuilderHolder builder) {
if (InfinispanUtils.isRemoteInfinispan())
return false;
var transportConfig = builder.getGlobalConfigurationBuilder().transport();
if (transportConfig.getTransport() == null)
return false;
String transportStack = Configuration.getRawValue("kc.cache-stack");
if (transportStack != null && !isJdbcPingStack(transportStack))
return false;
var stackXmlAttribute = transportConfig.defaultTransport().attributes().attribute(STACK);
return !stackXmlAttribute.isModified() || isJdbcPingStack(stackXmlAttribute.get());
}
public EmbeddedCacheManager getOrCreateEmbeddedCacheManager(KeycloakSession keycloakSession) {
if (cacheManagerFuture != null)
return join(cacheManagerFuture);
if (cacheManager == null) {
if (cacheManagerFuture == null) {
synchronized (this) {
if (cacheManager == null) {
EntityManager em = keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager();
cacheManager = jdbcCacheManagerFunction.apply(em);
if (cacheManagerFuture == null) {
cacheManagerFuture = CompletableFuture.completedFuture(startEmbeddedCacheManager(keycloakSession));
}
}
}
return cacheManager;
return join(cacheManagerFuture);
}
public RemoteCacheManager getOrCreateRemoteCacheManager() {
@ -327,24 +288,14 @@ public class CacheManagerFactory {
admin.reindexCache(cacheName);
}
private EmbeddedCacheManager startEmbeddedCacheManager(ConfigurationBuilderHolder builder, EntityManager em) {
private EmbeddedCacheManager startEmbeddedCacheManager(KeycloakSession session) {
logger.info("Starting Infinispan embedded cache manager");
var builder = jGroupsConfigurator.holder();
// We must disable the Infinispan default ShutdownHook as we manage the EmbeddedCacheManager lifecycle explicitly
// with #shutdown and multiple calls to EmbeddedCacheManager#stop can lead to Exceptions being thrown
builder.getGlobalConfigurationBuilder().shutdown().hookBehavior(ShutdownHookBehavior.DONT_REGISTER);
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 (Configuration.isTrue(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)) {
builder.getGlobalConfigurationBuilder().metrics().histograms(true);
}
builder.getNamedConfigurationBuilders().forEach((s, configurationBuilder) -> configurationBuilder.statistics().enabled(true));
}
Marshalling.configure(builder.getGlobalConfigurationBuilder());
assertAllCachesAreConfigured(builder,
// skip revision caches, those are defined by DefaultInfinispanConnectionProviderFactory
@ -370,19 +321,36 @@ public class CacheManagerFactory {
// embedded mode!
assertAllCachesAreConfigured(builder, Arrays.stream(CLUSTERED_CACHE_NAMES));
if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) {
configureTransportStack(builder, em);
if (jGroupsConfigurator.isLocal()) {
throw new RuntimeException("Unable to use clustered cache with local mode.");
}
configureRemoteStores(builder);
}
jGroupsConfigurator.configure(session);
configureCacheMaxCount(builder, CachingOptions.CLUSTERED_MAX_COUNT_CACHES);
configureSessionsCaches(builder);
validateWorkCacheConfiguration(builder);
}
configureCacheMaxCount(builder, CachingOptions.LOCAL_MAX_COUNT_CACHES);
checkForRemoteStores(builder);
configureMetrics(builder);
return new DefaultCacheManager(builder, isStartEagerly());
}
private static void configureMetrics(ConfigurationBuilderHolder holder) {
if (Configuration.isTrue(MetricsOptions.METRICS_ENABLED)) {
holder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class);
holder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry);
holder.getGlobalConfigurationBuilder().cacheContainer().statistics(true);
holder.getGlobalConfigurationBuilder().metrics().namesAsTags(true);
if (Configuration.isTrue(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)) {
holder.getGlobalConfigurationBuilder().metrics().histograms(true);
}
holder.getNamedConfigurationBuilders().forEach((s, configurationBuilder) -> configurationBuilder.statistics().enabled(true));
}
}
private static boolean isRemoteTLSEnabled() {
return Configuration.isTrue(CachingOptions.CACHE_REMOTE_TLS_ENABLED);
}
@ -416,102 +384,6 @@ public class CacheManagerFactory {
return Integer.getInteger("kc.cache-ispn-start-timeout", 120);
}
private static void configureTransportStack(ConfigurationBuilderHolder builder, EntityManager em) {
var transportConfig = builder.getGlobalConfigurationBuilder().transport();
if (Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED)) {
validateTlsAvailable(transportConfig.build());
var tls = new TLS()
.enabled(true)
.setKeystorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY))
.setKeystorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY))
.setKeystoreType("pkcs12")
.setTruststorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY))
.setTruststorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY))
.setTruststoreType("pkcs12")
.setClientAuth(TLSClientAuth.NEED)
.setProtocols(new String[]{"TLSv1.3"});
transportConfig.addProperty(JGroupsTransport.SOCKET_FACTORY, tls.createSocketFactory());
logger.info("MTLS enabled for communications for embedded caches");
}
String transportStack = Configuration.getRawValue("kc.cache-stack");
if (transportStack != null && !transportStack.isBlank() && !isJdbcPingStack(transportStack)) {
warnDeprecatedStack(transportStack);
transportConfig.defaultTransport().stack(transportStack);
return;
}
var stackXmlAttribute = transportConfig.defaultTransport().attributes().attribute(STACK);
// If the user has explicitly defined a transport stack that is not jdbc-ping or jdbc-ping-udp, return
if (stackXmlAttribute.isModified() && !isJdbcPingStack(stackXmlAttribute.get())) {
warnDeprecatedStack(stackXmlAttribute.get());
return;
}
var stackName = transportStack != null ?
transportStack :
stackXmlAttribute.isModified() ? stackXmlAttribute.get() : "jdbc-ping";
warnDeprecatedStack(stackName);
var udp = stackName.endsWith("udp");
var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em);
var attributes = Map.of(
// Leave initialize_sql blank as table is already created by Keycloak
"initialize_sql", "",
// Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default
// "cluster" cannot be used with Oracle DB as it's a reserved word.
"clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName),
"delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName),
"insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName),
"select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName),
"remove_all_data_on_view_change", "true",
"register_shutdown_hook", "false",
"stack.combine", "REPLACE",
"stack.position", udp ? "PING" : "MPING"
);
var stack = List.of(new ProtocolConfiguration(JDBC_PING2.class.getSimpleName(), attributes));
builder.addJGroupsStack(new EmbeddedJGroupsChannelConfigurator(stackName, stack, null), udp ? "udp" : "tcp");
Supplier<DataSource> dataSourceSupplier = Arc.container().select(AgroalDataSource.class)::get;
transportConfig.addProperty(JGroupsTransport.DATA_SOURCE, dataSourceSupplier);
transportConfig.defaultTransport().stack(stackName);
}
private static void warnDeprecatedStack(String stackName) {
switch (stackName) {
case "jdbc-ping-udp":
case "tcp":
case "udp":
case "azure":
case "ec2":
case "google":
Logger.getLogger(CacheManagerFactory.class).warnf("Stack '%s' is deprecated. We recommend to use 'jdbc-ping' instead", stackName);
}
}
private static boolean isJdbcPingStack(String stackName) {
return "jdbc-ping".equals(stackName) || "jdbc-ping-udp".equals(stackName);
}
private static void validateTlsAvailable(GlobalConfiguration config) {
var stackName = config.transport().stack();
if (stackName == null) {
// unable to validate
return;
}
for (var protocol : config.transport().jgroups().configurator(stackName).getProtocolStack()) {
var name = protocol.getProtocolName();
if (name.equals(UDP.class.getSimpleName()) ||
name.equals(UDP.class.getName()) ||
name.equals(TCP_NIO2.class.getSimpleName()) ||
name.equals(TCP_NIO2.class.getName())) {
throw new RuntimeException("Cache TLS is not available with protocol " + name);
}
}
}
private static 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()) {
@ -655,7 +527,7 @@ public class CacheManagerFactory {
}
}
private static String requiredStringProperty(String propertyName) {
public static String requiredStringProperty(String propertyName) {
return Configuration.getOptionalKcValue(propertyName).orElseThrow(() -> new RuntimeException("Property " + propertyName + " required but not specified"));
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups;
import java.util.ArrayList;
import java.util.List;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.keycloak.config.CachingOptions;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl.FileJGroupsTlsConfigurator;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl.JGroupsJdbcPingStackConfigurator;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl.JpaJGroupsTlsConfigurator;
/**
* Configures the JGroups stacks before starting Infinispan.
*/
public class JGroupsConfigurator {
private final ConfigurationBuilderHolder holder;
private final List<JGroupsStackConfigurator> stackConfiguratorList;
private JGroupsConfigurator(ConfigurationBuilderHolder holder, List<JGroupsStackConfigurator> stackConfiguratorList) {
this.holder = holder;
this.stackConfiguratorList = stackConfiguratorList;
}
private static void createJdbcPingConfigurator(ConfigurationBuilderHolder holder, List<JGroupsStackConfigurator> configurator) {
var stackXmlAttribute = JGroupsUtil.transportStackOf(holder);
if (stackXmlAttribute.isModified() && !isJdbcPingStack(stackXmlAttribute.get())) {
CacheManagerFactory.logger.debugf("Custom stack configured (%s). JDBC_PING discovery disabled.", stackXmlAttribute.get());
return;
}
CacheManagerFactory.logger.debug("JDBC_PING discovery enabled.");
if (!stackXmlAttribute.isModified()) {
// defaults to jdbc-ping
JGroupsUtil.transportOf(holder).stack("jdbc-ping");
}
configurator.add(JGroupsJdbcPingStackConfigurator.INSTANCE);
}
private static boolean isJdbcPingStack(String stackName) {
return "jdbc-ping".equals(stackName) || "jdbc-ping-udp".equals(stackName);
}
private static void createTlsConfigurator(List<JGroupsStackConfigurator> configurator) {
if (!Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED)) {
CacheManagerFactory.logger.debug("JGroups encryption disabled.");
return;
}
if (Configuration.isBlank(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE) && Configuration.isBlank(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE)) {
CacheManagerFactory.logger.debug("JGroups encryption enabled. Neither KeyStore and Truststore present, using the certificates from database.");
configurator.add(JpaJGroupsTlsConfigurator.INSTANCE);
return;
}
CacheManagerFactory.logger.debug("JGroups encryption enabled. KeyStore or Truststore present.");
configurator.add(FileJGroupsTlsConfigurator.INSTANCE);
}
private static boolean isLocal(ConfigurationBuilderHolder holder) {
return JGroupsUtil.transportOf(holder) == null;
}
public static JGroupsConfigurator create(ConfigurationBuilderHolder holder) {
if (InfinispanUtils.isRemoteInfinispan() || isLocal(holder)) {
CacheManagerFactory.logger.debug("Multi Site or local mode. Skipping JGroups configuration.");
return new JGroupsConfigurator(holder, List.of());
}
// Configure stack from CLI options to Global Configuration
Configuration.getOptionalKcValue(CachingOptions.CACHE_STACK).ifPresent(JGroupsUtil.transportOf(holder)::stack);
var configurator = new ArrayList<JGroupsStackConfigurator>(2);
createJdbcPingConfigurator(holder, configurator);
createTlsConfigurator(configurator);
return new JGroupsConfigurator(holder, List.copyOf(configurator));
}
/**
* @return The {@link ConfigurationBuilderHolder} with the current Infinispan configuration.
*/
public ConfigurationBuilderHolder holder() {
return holder;
}
/**
* @return {@code true} if it requires a {@link KeycloakSession} to perform the stack configuration.
*/
public boolean requiresKeycloakSession() {
return stackConfiguratorList.stream().anyMatch(JGroupsStackConfigurator::requiresKeycloakSession);
}
/**
* @return {@code true} if Keycloak is run in local mode (development mode for example) and JGroups won't be used.
*/
public boolean isLocal() {
return isLocal(holder);
}
/**
* Configures the JGroups stack.
*
* @param session The {@link KeycloakSession}. It is {@code null} when {@link #requiresKeycloakSession()} returns
* {@code false}.
*/
public void configure(KeycloakSession session) {
if (InfinispanUtils.isRemoteInfinispan() || isLocal()) {
return;
}
stackConfiguratorList.forEach(jGroupsStackConfigurator -> jGroupsStackConfigurator.configure(holder, session));
JGroupsUtil.warnDeprecatedStack(holder);
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.keycloak.models.KeycloakSession;
/**
* Interface to configure a JGroups Stack before Keycloak starts the embedded Infinispan.
*/
public interface JGroupsStackConfigurator {
/**
* @return {@code true} if this configuration requires the sessions, for example, to access a database.
*/
boolean requiresKeycloakSession();
/**
* Configures the stack in {@code holder}.
* <p>
* The {@code session} is not {@code null} when {@link #requiresKeycloakSession()} returns {@code true}.
*
* @param holder The Infinispan {@link ConfigurationBuilderHolder}.
* @param session The current {@link KeycloakSession}. It may be {@code null};
*/
void configure(ConfigurationBuilderHolder holder, KeycloakSession session);
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups;
import org.infinispan.commons.configuration.attributes.Attribute;
import org.infinispan.configuration.global.TransportConfigurationBuilder;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.jgroups.protocols.TCP_NIO2;
import org.jgroups.protocols.UDP;
import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory;
import static org.infinispan.configuration.global.TransportConfiguration.STACK;
public final class JGroupsUtil {
private JGroupsUtil() {
}
public static TransportConfigurationBuilder transportOf(ConfigurationBuilderHolder holder) {
return holder.getGlobalConfigurationBuilder().transport();
}
public static Attribute<String> transportStackOf(ConfigurationBuilderHolder holder) {
var transport = transportOf(holder);
assert transport != null;
return transport.attributes().attribute(STACK);
}
public static void warnDeprecatedStack(ConfigurationBuilderHolder holder) {
var stackName = transportStackOf(holder).get();
switch (stackName) {
case "jdbc-ping-udp":
case "tcp":
case "udp":
case "azure":
case "ec2":
case "google":
CacheManagerFactory.logger.warnf("Stack '%s' is deprecated. We recommend to use 'jdbc-ping' instead", stackName);
}
}
public static void validateTlsAvailable(ConfigurationBuilderHolder holder) {
var stackName = transportStackOf(holder).get();
if (stackName == null) {
// unable to validate
return;
}
var config = transportOf(holder).build();
for (var protocol : config.transport().jgroups().configurator(stackName).getProtocolStack()) {
var name = protocol.getProtocolName();
if (name.equals(UDP.class.getSimpleName()) ||
name.equals(UDP.class.getName()) ||
name.equals(TCP_NIO2.class.getSimpleName()) ||
name.equals(TCP_NIO2.class.getName())) {
throw new RuntimeException("Cache TLS is not available with protocol " + name);
}
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.jgroups.util.SocketFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsStackConfigurator;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsUtil;
abstract class BaseJGroupsTlsConfigurator implements JGroupsStackConfigurator {
@Override
public void configure(ConfigurationBuilderHolder holder, KeycloakSession session) {
var factory = createSocketFactory(session);
JGroupsUtil.transportOf(holder).addProperty(JGroupsTransport.SOCKET_FACTORY, factory);
JGroupsUtil.validateTlsAvailable(holder);
CacheManagerFactory.logger.info("JGroups Encryption enabled (mTLS).");
}
abstract SocketFactory createSocketFactory(KeycloakSession session);
}

View File

@ -0,0 +1,118 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.common.util.PemUtils;
/**
* JPA entity to store the {@link X509Certificate} and {@link KeyPair}.
*/
@SuppressWarnings("unused")
public class CertificateEntity {
@JsonProperty("prvKey")
private String privateKeyPem;
@JsonProperty("pubKey")
private String publicKeyPem;
@JsonProperty("crt")
private String certificatePem;
public CertificateEntity() {
}
public CertificateEntity(String privateKeyPem, String publicKeyPem, String certificatePem) {
this.privateKeyPem = Objects.requireNonNull(privateKeyPem);
this.publicKeyPem = Objects.requireNonNull(publicKeyPem);
this.certificatePem = Objects.requireNonNull(certificatePem);
}
public String getCertificatePem() {
return certificatePem;
}
public void setCertificatePem(String certificatePem) {
this.certificatePem = certificatePem;
}
public String getPrivateKeyPem() {
return privateKeyPem;
}
public void setPrivateKeyPem(String privateKeyPem) {
this.privateKeyPem = privateKeyPem;
}
public String getPublicKeyPem() {
return publicKeyPem;
}
public void setPublicKeyPem(String publicKeyPem) {
this.publicKeyPem = publicKeyPem;
}
@JsonIgnore
public void setCertificate(X509Certificate certificate) {
Objects.requireNonNull(certificate);
setCertificatePem(PemUtils.encodeCertificate(certificate));
}
@JsonIgnore
public void setKeyPair(KeyPair keyPair) {
Objects.requireNonNull(keyPair);
setPrivateKeyPem(PemUtils.encodeKey(keyPair.getPrivate()));
setPublicKeyPem(PemUtils.encodeKey(keyPair.getPublic()));
}
@JsonIgnore
public X509Certificate getCertificate() {
return PemUtils.decodeCertificate(getCertificatePem());
}
@JsonIgnore
public KeyPair getKeyPair() {
var prv = PemUtils.decodePrivateKey(getPrivateKeyPem());
var pub = PemUtils.decodePublicKey(getPublicKeyPem());
return new KeyPair(pub, prv);
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
CertificateEntity that = (CertificateEntity) o;
return Objects.equals(privateKeyPem, that.privateKeyPem) &&
Objects.equals(publicKeyPem, that.publicKeyPem) &&
Objects.equals(certificatePem, that.certificatePem);
}
@Override
public int hashCode() {
int result = Objects.hashCode(privateKeyPem);
result = 31 * result + Objects.hashCode(publicKeyPem);
result = 31 * result + Objects.hashCode(certificatePem);
return result;
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl;
import org.jgroups.util.SocketFactory;
import org.jgroups.util.TLS;
import org.jgroups.util.TLSClientAuth;
import org.keycloak.models.KeycloakSession;
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.quarkus.runtime.storage.infinispan.CacheManagerFactory.requiredStringProperty;
/**
* JGroups mTLS configuration using the provided KeyStore and TrustStore files.
*/
public class FileJGroupsTlsConfigurator extends BaseJGroupsTlsConfigurator {
public static final FileJGroupsTlsConfigurator INSTANCE = new FileJGroupsTlsConfigurator();
@Override
SocketFactory createSocketFactory(KeycloakSession ignored) {
var tls = new TLS()
.enabled(true)
.setKeystorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY))
.setTruststorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY))
.setKeystorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY))
.setTruststorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY))
.setKeystoreType("pkcs12")
.setTruststoreType("pkcs12")
.setClientAuth(TLSClientAuth.NEED)
.setProtocols(new String[]{"TLSv1.3"});
return tls.createSocketFactory();
}
@Override
public boolean requiresKeycloakSession() {
return false;
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import io.agroal.api.AgroalDataSource;
import io.quarkus.arc.Arc;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.jgroups.conf.ProtocolConfiguration;
import org.jgroups.protocols.JDBC_PING2;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsStackConfigurator;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsUtil;
import javax.sql.DataSource;
/**
* JGroups discovery configuration using {@link JDBC_PING2}.
*/
public class JGroupsJdbcPingStackConfigurator implements JGroupsStackConfigurator {
public static final JGroupsStackConfigurator INSTANCE = new JGroupsJdbcPingStackConfigurator();
private JGroupsJdbcPingStackConfigurator() {}
@Override
public boolean requiresKeycloakSession() {
return true;
}
@Override
public void configure(ConfigurationBuilderHolder holder, KeycloakSession session) {
var em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
var stackName = JGroupsUtil.transportStackOf(holder).get();
var isUdp = stackName.endsWith("udp");
var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em);
var stack = getProtocolConfigurations(tableName, isUdp ? "PING" : "MPING");
holder.addJGroupsStack(new EmbeddedJGroupsChannelConfigurator(stackName, stack, null), isUdp ? "udp" : "tcp");
Supplier<DataSource> dataSourceSupplier = Arc.container().select(AgroalDataSource.class)::get;
JGroupsUtil.transportOf(holder).addProperty(JGroupsTransport.DATA_SOURCE, dataSourceSupplier);
JGroupsUtil.transportOf(holder).stack(stackName);
CacheManagerFactory.logger.info("JGroups JDBC_PING discovery enabled.");
}
private static List<ProtocolConfiguration> getProtocolConfigurations(String tableName, String discoveryProtocol) {
var attributes = Map.of(
// Leave initialize_sql blank as table is already created by Keycloak
"initialize_sql", "",
// Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default
// "cluster" cannot be used with Oracle DB as it's a reserved word.
"clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName),
"delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName),
"insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName),
"select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName),
"remove_all_data_on_view_change", "true",
"register_shutdown_hook", "false",
"stack.combine", "REPLACE",
"stack.position", discoveryProtocol
);
return List.of(new ProtocolConfiguration(JDBC_PING2.class.getSimpleName(), attributes));
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.jgroups.util.DefaultSocketFactory;
import org.jgroups.util.SocketFactory;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Retry;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.configuration.ServerConfigStorageProvider;
import org.keycloak.util.JsonSerialization;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
/**
* JGroups mTLS configuration using certificates stored by {@link ServerConfigStorageProvider}.
*/
public class JpaJGroupsTlsConfigurator extends BaseJGroupsTlsConfigurator {
private static final char[] KEY_PASSWORD = "jgroups-password".toCharArray();
private static final String CERTIFICATE_ID = "crt_jgroups";
private static final String KEYSTORE_ALIAS = "jgroups";
private static final String JGROUPS_SUBJECT = "jgroups";
private static final String TLS_PROTOCOL_VERSION = "TLSv1.3";
private static final String TLS_PROTOCOL = "TLS";
private static final int STARTUP_RETRIES = 2;
private static final int STARTUP_RETRY_SLEEP_MILLIS = 10;
public static final JpaJGroupsTlsConfigurator INSTANCE = new JpaJGroupsTlsConfigurator();
@Override
public boolean requiresKeycloakSession() {
return true;
}
@Override
SocketFactory createSocketFactory(KeycloakSession session) {
var factory = session.getKeycloakSessionFactory();
return Retry.call(iteration -> KeycloakModelUtils.runJobInTransactionWithResult(factory, this::createSocketFactoryInTransaction), STARTUP_RETRIES, STARTUP_RETRY_SLEEP_MILLIS);
}
private SocketFactory createSocketFactoryInTransaction(KeycloakSession session) {
try {
var storage = session.getProvider(ServerConfigStorageProvider.class);
var data = fromJson(storage.loadOrCreate(CERTIFICATE_ID, JpaJGroupsTlsConfigurator::generateSelfSignedCertificate));
var km = createKeyManager(data.getKeyPair(), data.getCertificate());
var tm = createTrustManager(data.getCertificate());
var sslContext = SSLContext.getInstance(TLS_PROTOCOL);
sslContext.init(new KeyManager[]{km}, new TrustManager[]{tm}, null);
return createFromContext(sslContext);
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private X509ExtendedKeyManager createKeyManager(KeyPair keyPair, X509Certificate certificate) throws GeneralSecurityException, IOException {
var ks = CryptoIntegration.getProvider().getKeyStore(KeystoreUtil.KeystoreFormat.JKS);
ks.load(null, null);
ks.setKeyEntry(KEYSTORE_ALIAS, keyPair.getPrivate(), KEY_PASSWORD, new java.security.cert.Certificate[]{certificate});
var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, KEY_PASSWORD);
for (KeyManager km : kmf.getKeyManagers()) {
if (km instanceof X509ExtendedKeyManager) {
return (X509ExtendedKeyManager) km;
}
}
throw new GeneralSecurityException("Could not obtain an X509ExtendedKeyManager");
}
private X509ExtendedTrustManager createTrustManager(X509Certificate certificate) throws GeneralSecurityException, IOException {
var ks = CryptoIntegration.getProvider().getKeyStore(KeystoreUtil.KeystoreFormat.JKS);
ks.load(null, null);
ks.setCertificateEntry(KEYSTORE_ALIAS, certificate);
var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509ExtendedTrustManager) {
return (X509ExtendedTrustManager) tm;
}
}
throw new GeneralSecurityException("Could not obtain an X509TrustManager");
}
private static String generateSelfSignedCertificate() {
var keyPair = KeyUtils.generateRsaKeyPair(2048);
var certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, JGROUPS_SUBJECT);
var entity = new CertificateEntity();
entity.setCertificate(certificate);
entity.setKeyPair(keyPair);
return toJson(entity);
}
private static SocketFactory createFromContext(SSLContext context) {
DefaultSocketFactory socketFactory = new DefaultSocketFactory(context);
final SSLParameters serverParameters = new SSLParameters();
serverParameters.setProtocols(new String[]{TLS_PROTOCOL_VERSION});
serverParameters.setNeedClientAuth(true);
socketFactory.setServerSocketConfigurator(socket -> ((SSLServerSocket) socket).setSSLParameters(serverParameters));
return socketFactory;
}
private static String toJson(CertificateEntity entity) {
try {
return JsonSerialization.mapper.writeValueAsString(entity);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Should never happen!", e);
}
}
private static CertificateEntity fromJson(String json) {
try {
return JsonSerialization.mapper.readValue(json, CertificateEntity.class);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Should never happen!", e);
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.it.cli.dist;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.keycloak.config.CachingOptions;
import org.keycloak.config.Option;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.DryRun;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.utils.KeycloakDistribution;
@DistributionTest
public class CacheEmbeddedMtlsDistTest {
@DryRun
@Test
@RawDistOnly(reason = "Containers are immutable")
public void testCacheEmbeddedMtlsDisabled(KeycloakDistribution dist) {
for (var option : Arrays.asList(
CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE,
CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE,
CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD,
CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD
)) {
var result = dist.run("start-dev", "--cache=ispn", "--%s=a".formatted(option.getKey()));
result.assertError("Disabled option: '--%s'. Available only when property 'cache-embedded-mtls-enabled' is enabled.".formatted(option.getKey()));
}
}
@DryRun
@Test
@RawDistOnly(reason = "Containers are immutable")
public void testCacheEmbeddedMtlsFileValidation(KeycloakDistribution dist) {
doFileAndPasswordValidation(dist, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD);
doFileAndPasswordValidation(dist, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD);
}
@Test
@RawDistOnly(reason = "Containers are immutable")
public void testCacheEmbeddedMtlsEnabled(KeycloakDistribution dist) {
var result = dist.run("start-dev", "--cache=ispn", "--cache-embedded-mtls-enabled=true");
result.assertMessage("JGroups JDBC_PING discovery enabled.");
result.assertMessage("JGroups Encryption enabled (mTLS).");
}
private void doFileAndPasswordValidation(KeycloakDistribution dist, Option<String> fileOption, Option<String> passwordOption) {
var result = dist.run("start-dev", "--cache=ispn", "--cache-embedded-mtls-enabled=true", "--%s=file".formatted(fileOption.getKey()));
result.assertError("The option '%s' requires '%s' to be enabled.".formatted(fileOption.getKey(), passwordOption.getKey()));
result = dist.run("start-dev", "--cache=ispn", "--cache-embedded-mtls-enabled=true", "--%s=secret".formatted(passwordOption.getKey()));
result.assertError("The option '%s' requires '%s' to be enabled.".formatted(passwordOption.getKey(), fileOption.getKey()));
}
}

View File

@ -99,18 +99,11 @@ public class OptionsDistTest {
result.assertMessage("- log-syslog-app-name: Available only when Syslog is activated.");
}
@Test
@Order(7)
@Launch({"start", "--db=dev-file", "--cache-embedded-mtls-enabled=true", "--http-enabled=true", "--hostname-strict=false"})
public void testCacheEmbeddedMtlsEnabled(LaunchResult result) {
assertTrue(result.getOutputStream().stream().anyMatch(s -> s.contains("Property cache-embedded-mtls-key-store-file required but not specified")));
}
// Start-dev should be executed as last tests - build is done for development mode
@DryRun
@Test
@Order(8)
@Order(7)
@Launch({"start-dev", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
@ -118,7 +111,7 @@ public class OptionsDistTest {
@DryRun
@Test
@Order(9)
@Order(8)
@Launch({"start-dev", "--log=console", "--log-file-output=json"})
public void testServerDoesNotStartDevIfDisabledFileLogOption(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
@ -127,7 +120,7 @@ public class OptionsDistTest {
@DryRun
@Test
@Order(10)
@Order(9)
@Launch({"start-dev", "--log=file", "--log-file-output=json", "--log-console-color=true"})
public void testServerStartDevIfEnabledFileLogOption(LaunchResult result) {
assertEquals(0, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());

View File

@ -38,18 +38,6 @@ Cache:
The maximum number of entries that can be stored in-memory by the keys cache.
--cache-embedded-mtls-enabled <true|false>
Encrypts the network communication between Keycloak servers. Default: false.
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -41,15 +41,19 @@ Cache:
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
conf/ directory. Available only when property 'cache-embedded-mtls-enabled'
is enabled..
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
The password to access the Keystore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
'cache-mtls-truststore.p12' under conf/ directory. Available only when
property 'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
The password to access the Truststore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -39,18 +39,6 @@ Cache:
The maximum number of entries that can be stored in-memory by the keys cache.
--cache-embedded-mtls-enabled <true|false>
Encrypts the network communication between Keycloak servers. Default: false.
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -42,15 +42,19 @@ Cache:
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
conf/ directory. Available only when property 'cache-embedded-mtls-enabled'
is enabled..
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
The password to access the Keystore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
'cache-mtls-truststore.p12' under conf/ directory. Available only when
property 'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
The password to access the Truststore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -39,18 +39,6 @@ Cache:
The maximum number of entries that can be stored in-memory by the keys cache.
--cache-embedded-mtls-enabled <true|false>
Encrypts the network communication between Keycloak servers. Default: false.
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -42,15 +42,19 @@ Cache:
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
conf/ directory. Available only when property 'cache-embedded-mtls-enabled'
is enabled..
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
The password to access the Keystore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
'cache-mtls-truststore.p12' under conf/ directory. Available only when
property 'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
The password to access the Truststore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -38,18 +38,6 @@ Cache:
The maximum number of entries that can be stored in-memory by the keys cache.
--cache-embedded-mtls-enabled <true|false>
Encrypts the network communication between Keycloak servers. Default: false.
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -41,15 +41,19 @@ Cache:
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
conf/ directory. Available only when property 'cache-embedded-mtls-enabled'
is enabled..
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
The password to access the Keystore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
'cache-mtls-truststore.p12' under conf/ directory. Available only when
property 'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
The password to access the Truststore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -36,18 +36,6 @@ Cache:
The maximum number of entries that can be stored in-memory by the keys cache.
--cache-embedded-mtls-enabled <true|false>
Encrypts the network communication between Keycloak servers. Default: false.
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -39,15 +39,19 @@ Cache:
--cache-embedded-mtls-key-store-file <file>
The Keystore file path. The Keystore must contain the certificate to use by
the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under
conf/ directory.
conf/ directory. Available only when property 'cache-embedded-mtls-enabled'
is enabled..
--cache-embedded-mtls-key-store-password <password>
The password to access the Keystore.
The password to access the Keystore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-file <file>
The Truststore file path. It should contain the trusted certificates or the
Certificate Authority that signed the certificates. By default, it lookup
'cache-mtls-truststore.p12' under conf/ directory.
'cache-mtls-truststore.p12' under conf/ directory. Available only when
property 'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-mtls-trust-store-password <password>
The password to access the Truststore.
The password to access the Truststore. Available only when property
'cache-embedded-mtls-enabled' is enabled..
--cache-embedded-offline-client-sessions-max-count <max-count>
The maximum number of entries that can be stored in-memory by the
offlineClientSessions cache. Available only when embedded Infinispan

View File

@ -80,7 +80,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
protected KeycloakQuarkusConfiguration configuration;
protected List<String> additionalBuildArgs = Collections.emptyList();
protected Map<String, List<String>> spis = new HashMap<String, List<String>>();
protected Map<String, List<String>> spis = new HashMap<>();
@Override
public Class<KeycloakQuarkusConfiguration> getConfigurationClass() {
@ -237,6 +237,10 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
System.setProperty("kc.cache-remote-create-caches", "true");
}
if (configuration.isJgroupsMtls()) {
commands.add("--cache-embedded-mtls-enabled=true");
}
return commands;
}
@ -383,12 +387,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
}
private HostnameVerifier createInsecureHostnameVerifier() {
return new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
};
return (s, sslSession) -> true;
}
private SSLSocketFactory createInsecureSslSocketFactory() throws IOException {

View File

@ -1,17 +1,12 @@
package org.keycloak.testsuite.arquillian.containers;
import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.arquillian.container.spi.ConfigurationException;
import org.jboss.arquillian.container.spi.client.container.ContainerConfiguration;
import org.jboss.logging.Logger;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* @author mhajas
@ -50,6 +45,8 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration {
private String enabledFeatures;
private String disabledFeatures;
private boolean jgroupsMtls;
@Override
public void validate() throws ConfigurationException {
int basePort = getBindHttpPort();
@ -235,4 +232,12 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration {
public void setDisabledFeatures(String disabledFeatures) {
this.disabledFeatures = disabledFeatures;
}
public boolean isJgroupsMtls() {
return jgroupsMtls;
}
public void setJgroupsMtls(boolean jgroupsMtls) {
this.jgroupsMtls = jgroupsMtls;
}
}

View File

@ -676,6 +676,7 @@
</property>
<property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property>
<property name="outputToConsole">true</property>
<property name="jgroupsMtls">${auth.server.jgroups.mtls}</property>
</configuration>
</container>
<container qualifier="auth-server-quarkus-backend2" mode="manual" >
@ -700,6 +701,7 @@
</property>
<property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property>
<property name="outputToConsole">true</property>
<property name="jgroupsMtls">${auth.server.jgroups.mtls}</property>
</configuration>
</container>
</group>

View File

@ -93,6 +93,7 @@
<auth.server.remote>false</auth.server.remote>
<auth.server.quarkus>false</auth.server.quarkus>
<auth.server.quarkus.embedded>false</auth.server.quarkus.embedded>
<auth.server.jgroups.mtls>false</auth.server.jgroups.mtls>
<auth.server.profile/>
<auth.server.feature/>
@ -452,6 +453,7 @@
<auth.server.keystore.type>${auth.server.keystore.type}</auth.server.keystore.type>
<auth.server.java.security.file>${auth.server.java.security.file}</auth.server.java.security.file>
<auth.server.jvm.args.extra>${auth.server.jvm.args.extra}</auth.server.jvm.args.extra>
<auth.server.jgroups.mtls>${auth.server.jgroups.mtls}</auth.server.jgroups.mtls>
<auth.server.profile>${auth.server.profile}</auth.server.profile>
<auth.server.feature>${auth.server.feature}</auth.server.feature>