Skip to content

Commit

Permalink
new truststore config (#144)
Browse files Browse the repository at this point in the history
* introduce new truststore configuration, also used by mTLS client auth
* separate truststore and https-client-auth configuration
  • Loading branch information
dasniko committed May 30, 2024
1 parent 6494808 commit 861ffe6
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@ public abstract class ExtendableKeycloakContainer<SELF extends ExtendableKeycloa
private static final String KEYCLOAK_ADMIN_USER = "admin";
private static final String KEYCLOAK_ADMIN_PASSWORD = "admin";
private static final String KEYCLOAK_CONTEXT_PATH = "";
private static final String KEYCLOAK_HOME_DIR = "/opt/keycloak";
private static final String KEYCLOAK_CONF_DIR = KEYCLOAK_HOME_DIR + "/conf";

private static final String DEFAULT_KEYCLOAK_PROVIDERS_NAME = "providers.jar";
private static final String DEFAULT_KEYCLOAK_PROVIDERS_LOCATION = "/opt/keycloak/providers";
private static final String DEFAULT_REALM_IMPORT_FILES_LOCATION = "/opt/keycloak/data/import/";
private static final String DEFAULT_KEYCLOAK_PROVIDERS_LOCATION = KEYCLOAK_HOME_DIR + "/providers";
private static final String DEFAULT_REALM_IMPORT_FILES_LOCATION = KEYCLOAK_HOME_DIR + "/data/import/";

private static final String KEYSTORE_FILE_IN_CONTAINER = "/opt/keycloak/conf/server.keystore";
private static final String TRUSTSTORE_FILE_IN_CONTAINER = "/opt/keycloak/conf/server.truststore";
private static final String KEYSTORE_FILE_IN_CONTAINER = KEYCLOAK_CONF_DIR + "/server.keystore";
private static final String TRUSTSTORE_FILE_IN_CONTAINER = KEYCLOAK_CONF_DIR + "/server.truststore";

private String adminUsername = KEYCLOAK_ADMIN_USER;
private String adminPassword = KEYCLOAK_ADMIN_PASSWORD;
Expand All @@ -97,6 +99,7 @@ public abstract class ExtendableKeycloakContainer<SELF extends ExtendableKeycloa
private String tlsKeystorePassword;
private String tlsTruststoreFilename;
private String tlsTruststorePassword;
private List<String> tlsTrustedCertificateFilenames;
private boolean useTls = false;
private boolean disabledCaching = false;
private boolean metricsEnabled = false;
Expand Down Expand Up @@ -160,8 +163,8 @@ protected void configure() {
withEnv("JAVA_OPTS_KC_HEAP", "-XX:InitialRAMPercentage=%d -XX:MaxRAMPercentage=%d".formatted(initialRamPercentage, maxRamPercentage));

if (useTls && isNotBlank(tlsCertificateFilename)) {
String tlsCertFilePath = "/opt/keycloak/conf/tls.crt";
String tlsCertKeyFilePath = "/opt/keycloak/conf/tls.key";
String tlsCertFilePath = KEYCLOAK_CONF_DIR + "/tls.crt";
String tlsCertKeyFilePath = KEYCLOAK_CONF_DIR + "/tls.key";
withCopyFileToContainer(MountableFile.forClasspathResource(tlsCertificateFilename), tlsCertFilePath);
withCopyFileToContainer(MountableFile.forClasspathResource(tlsCertificateKeyFilename), tlsCertKeyFilePath);
withEnv("KC_HTTPS_CERTIFICATE_FILE", tlsCertFilePath);
Expand All @@ -175,8 +178,17 @@ protected void configure() {
withCopyFileToContainer(MountableFile.forClasspathResource(tlsTruststoreFilename), TRUSTSTORE_FILE_IN_CONTAINER);
withEnv("KC_HTTPS_TRUST_STORE_FILE", TRUSTSTORE_FILE_IN_CONTAINER);
withEnv("KC_HTTPS_TRUST_STORE_PASSWORD", tlsTruststorePassword);
withEnv("KC_HTTPS_CLIENT_AUTH", httpsClientAuth.toString());
}
if (isNotEmpty(tlsTrustedCertificateFilenames)) {
List<String> truststorePaths = new ArrayList<>();
tlsTrustedCertificateFilenames.forEach(certificateFilename -> {
String certPathInContainer = KEYCLOAK_CONF_DIR + (certificateFilename.startsWith("/") ? "" : "/") + certificateFilename;
withCopyFileToContainer(MountableFile.forClasspathResource(certificateFilename), certPathInContainer);
truststorePaths.add(certPathInContainer);
});
withEnv("KC_TRUSTSTORE_PATHS", String.join(",", truststorePaths));
}
withEnv("KC_HTTPS_CLIENT_AUTH", httpsClientAuth.toString());

withEnv("KC_METRICS_ENABLED", Boolean.toString(metricsEnabled));
withEnv("KC_HEALTH_ENABLED", Boolean.toString(Boolean.TRUE));
Expand Down Expand Up @@ -401,6 +413,10 @@ public SELF useTlsKeystore(String tlsKeystoreFilename, String tlsKeystorePasswor
return self();
}

/**
* @deprecated Will be removed soon! Use {@link #withTrustedCertificates(List)} and {@link #withHttpsClientAuth(HttpsClientAuth)} instead.
*/
@Deprecated(forRemoval = true)
public SELF useMutualTls(String tlsTruststoreFilename, String tlsTruststorePassword, HttpsClientAuth httpsClientAuth) {
requireNonNull(tlsTruststoreFilename, "tlsTruststoreFilename must not be null");
requireNonNull(tlsTruststorePassword, "tlsTruststorePassword must not be null");
Expand All @@ -412,6 +428,32 @@ public SELF useMutualTls(String tlsTruststoreFilename, String tlsTruststorePassw
return self();
}

/**
* Configure the Keycloak Truststore to communicate through TLS.
*
* @param tlsTrustedCertificateFilenames List of pkcs12 (p12 or pfx file extensions), PEM files, or directories containing those files
* that will be used as a system truststore.
* @return self
*/
public SELF withTrustedCertificates(List<String> tlsTrustedCertificateFilenames) {
requireNonNull(tlsTrustedCertificateFilenames, "tlsTrustCertificateFilenames must not be null");
this.tlsTrustedCertificateFilenames = tlsTrustedCertificateFilenames;
return self();
}

/**
* Configures the server to require/request client authentication.
*
* @param httpsClientAuth The http-client-auth mode
* @return self
*/
public SELF withHttpsClientAuth(HttpsClientAuth httpsClientAuth) {
requireNonNull(httpsClientAuth, "httpsClientAuth must not be null");
this.httpsClientAuth = httpsClientAuth;
this.useTls = true;
return self();
}

public SELF withVerboseOutput() {
this.useVerbose = true;
return self();
Expand Down Expand Up @@ -550,4 +592,8 @@ private boolean isNotBlank(String s) {
return s != null && !s.trim().isEmpty();
}

private boolean isNotEmpty(List<String> l) {
return l != null && !l.isEmpty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package dasniko.testcontainers.keycloak;

import io.restassured.RestAssured;
import io.restassured.config.SSLConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.net.ssl.SSLHandshakeException;
import java.time.Duration;

import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertThrows;


public class KeycloakContainerHttpsLegacyTest {

@BeforeEach
public void setup() {
RestAssured.reset();
}

@Test
public void shouldStartKeycloakWithMutualTlsRequestNoMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUEST)) {
keycloak.start();
checkTls(keycloak, "keycloak.jks", "keycloak");
}
}

@Test
public void shouldStartKeycloakWithMutualTlsRequestWithMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUEST)) {
keycloak.start();
checkMutualTls(keycloak, "keycloak.jks", "keycloak", "keycloak.jks", "keycloak");
}
}

@Test
public void shouldStartKeycloakWithMutualTlsRequiredWithMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUIRED)
.waitingFor(KeycloakContainer.LOG_WAIT_STRATEGY.withStartupTimeout(Duration.ofMinutes(2))) // this is hopefully only a workaround until mgmt port does not require mutual tls
) {
keycloak.start();
checkMutualTls(keycloak, "keycloak.jks", "keycloak", "keycloak.jks", "keycloak");
}
}

@Test
public void shouldStartKeycloakWithMutualTlsRequiredWithoutMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUIRED)
.waitingFor(KeycloakContainer.LOG_WAIT_STRATEGY.withStartupTimeout(Duration.ofMinutes(2))) // this is hopefully only a workaround until mgmt port does not require mutual tls
) {
keycloak.start();
assertThrows(SSLHandshakeException.class, () -> checkTls(keycloak, "keycloak.jks", "keycloak"));
}
}

@Test
public void shouldThrowNullPointerExceptionUponNullTlsTruststoreFilename() {
assertThrows(NullPointerException.class, () -> new KeycloakContainer().useMutualTls(null, null, HttpsClientAuth.NONE));
}

@Test
public void shouldThrowNullPointerExceptionUponNullHttpsClientAuth() {
assertThrows(NullPointerException.class, () -> new KeycloakContainer().useMutualTls("keycloak.jks", null, null));
}

private void checkTls(KeycloakContainer keycloak, String pathToTruststore, String truststorePassword) {
RestAssured.config = RestAssured.config().sslConfig(
SSLConfig.sslConfig().trustStore(pathToTruststore, truststorePassword)
);

assertThat(keycloak.getAuthServerUrl(), startsWith("https://"));

given()
.when().get(keycloak.getAuthServerUrl())
.then().statusCode(200);
}

private void checkMutualTls(KeycloakContainer keycloak, String pathToTruststore, String truststorePassword, String pathToKeystore,
String keystorePassword) {
RestAssured.config = RestAssured.config().sslConfig(
SSLConfig.sslConfig()
.trustStore(pathToTruststore, truststorePassword)
.keyStore(pathToKeystore, keystorePassword)
);

assertThat(keycloak.getAuthServerUrl(), startsWith("https://"));

given()
.when().get(keycloak.getAuthServerUrl())
.then().statusCode(200);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import org.keycloak.admin.client.resource.ServerInfoResource;

import javax.net.ssl.SSLHandshakeException;

import java.time.Duration;
import java.util.List;

import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -70,7 +70,9 @@ public void shouldStartKeycloakWithCustomTlsKeystore() {
public void shouldStartKeycloakWithMutualTlsRequestNoMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUEST)) {
.withTrustedCertificates(List.of("keycloak.crt"))
.withHttpsClientAuth(HttpsClientAuth.REQUEST)
) {
keycloak.start();
checkTls(keycloak, "keycloak.jks", "keycloak");
}
Expand All @@ -80,7 +82,9 @@ public void shouldStartKeycloakWithMutualTlsRequestNoMutualTls() {
public void shouldStartKeycloakWithMutualTlsRequestWithMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUEST)) {
.withTrustedCertificates(List.of("keycloak.crt"))
.withHttpsClientAuth(HttpsClientAuth.REQUEST)
) {
keycloak.start();
checkMutualTls(keycloak, "keycloak.jks", "keycloak", "keycloak.jks", "keycloak");
}
Expand All @@ -90,7 +94,8 @@ public void shouldStartKeycloakWithMutualTlsRequestWithMutualTls() {
public void shouldStartKeycloakWithMutualTlsRequiredWithMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUIRED)
.withTrustedCertificates(List.of("keycloak.crt"))
.withHttpsClientAuth(HttpsClientAuth.REQUIRED)
.waitingFor(KeycloakContainer.LOG_WAIT_STRATEGY.withStartupTimeout(Duration.ofMinutes(2))) // this is hopefully only a workaround until mgmt port does not require mutual tls
) {
keycloak.start();
Expand All @@ -102,7 +107,8 @@ public void shouldStartKeycloakWithMutualTlsRequiredWithMutualTls() {
public void shouldStartKeycloakWithMutualTlsRequiredWithoutMutualTls() {
try (KeycloakContainer keycloak = new KeycloakContainer()
.useTlsKeystore("keycloak.jks", "keycloak")
.useMutualTls("keycloak.jks", "keycloak", HttpsClientAuth.REQUIRED)
.withTrustedCertificates(List.of("keycloak.crt"))
.withHttpsClientAuth(HttpsClientAuth.REQUIRED)
.waitingFor(KeycloakContainer.LOG_WAIT_STRATEGY.withStartupTimeout(Duration.ofMinutes(2))) // this is hopefully only a workaround until mgmt port does not require mutual tls
) {
keycloak.start();
Expand All @@ -111,13 +117,13 @@ public void shouldStartKeycloakWithMutualTlsRequiredWithoutMutualTls() {
}

@Test
public void shouldThrowNullPointerExceptionUponNullTlsTruststoreFilename() {
assertThrows(NullPointerException.class, () -> new KeycloakContainer().useMutualTls(null, null, HttpsClientAuth.NONE));
public void shouldThrowNullPointerExceptionUponNullTlsTrustCertFilename() {
assertThrows(NullPointerException.class, () -> new KeycloakContainer().withTrustedCertificates(null));
}

@Test
public void shouldThrowNullPointerExceptionUponNullHttpsClientAuth() {
assertThrows(NullPointerException.class, () -> new KeycloakContainer().useMutualTls("keycloak.jks", null, null));
assertThrows(NullPointerException.class, () -> new KeycloakContainer().withHttpsClientAuth(null));
}

@Test
Expand Down

0 comments on commit 861ffe6

Please sign in to comment.