mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
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:
parent
8a03661ba0
commit
70e2a28ff9
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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> {
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -17,3 +17,4 @@
|
||||
|
||||
org.keycloak.storage.UserStorageProviderSpi
|
||||
org.keycloak.storage.federated.UserFederatedStorageProviderSpi
|
||||
org.keycloak.storage.configuration.ServerConfigurationStorageProviderSpi
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
73
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CacheEmbeddedMtlsDistTest.java
vendored
Normal file
73
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CacheEmbeddedMtlsDistTest.java
vendored
Normal 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()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user