dependencySet(group: 'com.squareup.okhttp3', version: '4.10.0') {
entry 'okhttp'
entry 'mockwebserver'
+ entry 'okhttp-tls'
}
dependency 'org.json:json:20230227'
dependency 'com.tngtech.java:junit-dataprovider:1.13.1'
testImplementation 'org.awaitility:awaitility'
testImplementation 'org.mockito:mockito-core'
testImplementation 'com.squareup.okhttp3:mockwebserver'
+ testImplementation 'com.squareup.okhttp3:okhttp-tls'
testImplementation 'commons-logging:commons-logging:1.2'
testImplementation project(':sonar-testing-harness')
}
package org.sonar.application;
import com.google.common.net.HostAndPort;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.application.cluster.AppNodesClusterHostsConsistency;
import org.sonar.process.cluster.hz.HazelcastMember;
import org.sonar.process.cluster.hz.HazelcastMemberBuilder;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_HZ_HOSTS;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_KUBERNETES;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_HOST;
.map(HostAndPort::fromString)
.collect(Collectors.toSet());
String searchPassword = props.value(CLUSTER_SEARCH_PASSWORD.getKey());
- return new EsConnectorImpl(hostAndPorts, searchPassword);
+ Path keyStorePath = Optional.ofNullable(props.value(CLUSTER_ES_HTTP_KEYSTORE.getKey())).map(Paths::get).orElse(null);
+ String keyStorePassword = props.value(CLUSTER_ES_HTTP_KEYSTORE_PASSWORD.getKey());
+ return new EsConnectorImpl(hostAndPorts, searchPassword, keyStorePath, keyStorePassword);
}
}
import static java.lang.String.format;
import static java.util.Collections.singleton;
import static org.sonar.application.es.EsKeyStoreCli.BOOTSTRAP_PASSWORD_PROPERTY_KEY;
+import static org.sonar.application.es.EsKeyStoreCli.HTTP_KEYSTORE_PASSWORD_PROPERTY_KEY;
import static org.sonar.application.es.EsKeyStoreCli.KEYSTORE_PASSWORD_PROPERTY_KEY;
import static org.sonar.application.es.EsKeyStoreCli.TRUSTSTORE_PASSWORD_PROPERTY_KEY;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_GRACEFUL_STOP_TIMEOUT_MS;
if (processId == ProcessId.ELASTICSEARCH) {
checkArgument(esInstallation != null, "Incorrect configuration EsInstallation is null");
EsConnectorImpl esConnector = new EsConnectorImpl(singleton(HostAndPort.fromParts(esInstallation.getHost(),
- esInstallation.getHttpPort())), esInstallation.getBootstrapPassword());
+ esInstallation.getHttpPort())), esInstallation.getBootstrapPassword(), esInstallation.getHttpKeyStoreLocation(),
+ esInstallation.getHttpKeyStorePassword().orElse(null));
return new EsManagedProcess(process, processId, esConnector);
} else {
ProcessCommands commands = allProcessesCommands.createAfterClean(processId.getIpcIndex());
pruneElasticsearchConfDirectory(confDir);
createElasticsearchConfDirectory(confDir);
- setupElasticsearchAuthentication(esInstallation);
+ setupElasticsearchSecurity(esInstallation);
esInstallation.getEsYmlSettings().writeToYmlSettingsFile(esInstallation.getElasticsearchYml());
esInstallation.getEsJvmOptions().writeToJvmOptionFile(esInstallation.getJvmOptions());
}
}
- private void setupElasticsearchAuthentication(EsInstallation esInstallation) {
+ private void setupElasticsearchSecurity(EsInstallation esInstallation) {
if (esInstallation.isSecurityEnabled()) {
- EsKeyStoreCli keyStoreCli = EsKeyStoreCli.getInstance(esInstallation)
- .store(BOOTSTRAP_PASSWORD_PROPERTY_KEY, esInstallation.getBootstrapPassword());
+ EsKeyStoreCli keyStoreCli = EsKeyStoreCli.getInstance(esInstallation);
- String esConfPath = esInstallation.getConfDirectory().getAbsolutePath();
+ setupElasticsearchAuthentication(esInstallation, keyStoreCli);
+ setupElasticsearchHttpEncryption(esInstallation, keyStoreCli);
- Path trustStoreLocation = esInstallation.getTrustStoreLocation();
- Path keyStoreLocation = esInstallation.getKeyStoreLocation();
- if (trustStoreLocation.equals(keyStoreLocation)) {
- copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
- } else {
- copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
- copyFile(keyStoreLocation, Paths.get(esConfPath, keyStoreLocation.toFile().getName()));
- }
+ keyStoreCli.executeWith(this::launchJava);
+ }
+ }
- esInstallation.getTrustStorePassword().ifPresent(s -> keyStoreCli.store(TRUSTSTORE_PASSWORD_PROPERTY_KEY, s));
- esInstallation.getKeyStorePassword().ifPresent(s -> keyStoreCli.store(KEYSTORE_PASSWORD_PROPERTY_KEY, s));
+ private static void setupElasticsearchAuthentication(EsInstallation esInstallation, EsKeyStoreCli keyStoreCli) {
+ keyStoreCli.store(BOOTSTRAP_PASSWORD_PROPERTY_KEY, esInstallation.getBootstrapPassword());
- keyStoreCli.executeWith(this::launchJava);
+ String esConfPath = esInstallation.getConfDirectory().getAbsolutePath();
+
+ Path trustStoreLocation = esInstallation.getTrustStoreLocation();
+ Path keyStoreLocation = esInstallation.getKeyStoreLocation();
+ if (trustStoreLocation.equals(keyStoreLocation)) {
+ copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
+ } else {
+ copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName()));
+ copyFile(keyStoreLocation, Paths.get(esConfPath, keyStoreLocation.toFile().getName()));
+ }
+
+ esInstallation.getTrustStorePassword().ifPresent(s -> keyStoreCli.store(TRUSTSTORE_PASSWORD_PROPERTY_KEY, s));
+ esInstallation.getKeyStorePassword().ifPresent(s -> keyStoreCli.store(KEYSTORE_PASSWORD_PROPERTY_KEY, s));
+ }
+
+ private static void setupElasticsearchHttpEncryption(EsInstallation esInstallation, EsKeyStoreCli keyStoreCli) {
+ if (esInstallation.isHttpEncryptionEnabled()) {
+ String esConfPath = esInstallation.getConfDirectory().getAbsolutePath();
+ Path httpKeyStoreLocation = esInstallation.getHttpKeyStoreLocation();
+ copyFile(httpKeyStoreLocation, Paths.get(esConfPath, httpKeyStoreLocation.toFile().getName()));
+ esInstallation.getHttpKeyStorePassword().ifPresent(s -> keyStoreCli.store(HTTP_KEYSTORE_PASSWORD_PROPERTY_KEY, s));
}
}
import com.google.common.net.HostAndPort;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
+import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.client.RequestOptions;
private final AtomicReference<RestHighLevelClient> restClient = new AtomicReference<>(null);
private final Set<HostAndPort> hostAndPorts;
private final String searchPassword;
+ private final Path keyStorePath;
+ private final String keyStorePassword;
- public EsConnectorImpl(Set<HostAndPort> hostAndPorts, @Nullable String searchPassword) {
+ public EsConnectorImpl(Set<HostAndPort> hostAndPorts, @Nullable String searchPassword, @Nullable Path keyStorePath,
+ @Nullable String keyStorePassword) {
this.hostAndPorts = hostAndPorts;
this.searchPassword = searchPassword;
+ this.keyStorePath = keyStorePath;
+ this.keyStorePassword = keyStorePassword;
}
@Override
private RestHighLevelClient buildRestHighLevelClient() {
HttpHost[] httpHosts = hostAndPorts.stream()
- .map(hostAndPort -> new HttpHost(hostAndPort.getHost(), hostAndPort.getPortOrDefault(9001)))
+ .map(this::toHttpHost)
.toArray(HttpHost[]::new);
if (LOG.isDebugEnabled()) {
BasicCredentialsProvider provider = getBasicCredentialsProvider(searchPassword);
httpClientBuilder.setDefaultCredentialsProvider(provider);
}
+
+ if (keyStorePath != null) {
+ SSLContext sslContext = getSSLContext(keyStorePath, keyStorePassword);
+ httpClientBuilder.setSSLContext(sslContext);
+ }
+
return httpClientBuilder;
});
return new RestHighLevelClient(builder);
}
+ private HttpHost toHttpHost(HostAndPort host) {
+ try {
+ String scheme = keyStorePath != null ? "https" : HttpHost.DEFAULT_SCHEME_NAME;
+ return new HttpHost(InetAddress.getByName(host.getHost()), host.getPortOrDefault(9001), scheme);
+ } catch (UnknownHostException e) {
+ throw new IllegalStateException("Can not resolve host [" + host + "]", e);
+ }
+ }
+
private static BasicCredentialsProvider getBasicCredentialsProvider(String searchPassword) {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(ES_USERNAME, searchPassword));
return provider;
}
+ private static SSLContext getSSLContext(Path keyStorePath, @Nullable String keyStorePassword) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("pkcs12");
+ try (InputStream is = Files.newInputStream(keyStorePath)) {
+ keyStore.load(is, keyStorePassword == null ? null : keyStorePassword.toCharArray());
+ }
+ SSLContextBuilder sslBuilder = SSLContexts.custom().loadTrustMaterial(keyStore, null);
+ return sslBuilder.build();
+ } catch (IOException | GeneralSecurityException e) {
+ throw new IllegalStateException("Failed to setup SSL context on ES client", e);
+ }
+ }
}
import org.sonar.process.Props;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE;
private final String keyStorePassword;
@Nullable
private final String trustStorePassword;
+ private final Path httpKeyStoreLocation;
+ @Nullable
+ private final String httpKeyStorePassword;
+ private final boolean httpEncryptionEnabled;
public EsInstallation(Props props) {
File sqHomeDir = props.nonNullValueAsFile(PATH_HOME.getKey());
this.keyStorePassword = props.value(CLUSTER_ES_KEYSTORE_PASSWORD.getKey());
this.trustStoreLocation = getPath(props.value(CLUSTER_ES_TRUSTSTORE.getKey()));
this.trustStorePassword = props.value(CLUSTER_ES_TRUSTSTORE_PASSWORD.getKey());
+ this.httpKeyStoreLocation = getPath(props.value(CLUSTER_ES_HTTP_KEYSTORE.getKey()));
+ this.httpKeyStorePassword = props.value(CLUSTER_ES_HTTP_KEYSTORE_PASSWORD.getKey());
+ this.httpEncryptionEnabled = securityEnabled && httpKeyStoreLocation != null;
}
private static Path getPath(@Nullable String path) {
public Optional<String> getTrustStorePassword() {
return Optional.ofNullable(trustStorePassword);
}
+
+ public Path getHttpKeyStoreLocation() {
+ return httpKeyStoreLocation;
+ }
+
+ public Optional<String> getHttpKeyStorePassword() {
+ return Optional.ofNullable(httpKeyStorePassword);
+ }
+
+ public boolean isHttpEncryptionEnabled() {
+ return httpEncryptionEnabled;
+ }
}
public static final String BOOTSTRAP_PASSWORD_PROPERTY_KEY = "bootstrap.password";
public static final String KEYSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.transport.ssl.keystore.secure_password";
public static final String TRUSTSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.transport.ssl.truststore.secure_password";
+ public static final String HTTP_KEYSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.http.ssl.keystore.secure_password";
private static final String MAIN_CLASS_NAME = "org.elasticsearch.launcher.CliToolLauncher";
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_DISCOVERY_SEED_HOSTS;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HOSTS;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
configureFileSystem(builder);
configureNetwork(builder);
configureCluster(builder);
- configureAuthentication(builder);
+ configureSecurity(builder);
configureOthers(builder);
LOGGER.info("Elasticsearch listening on [HTTP: {}:{}, TCP: {}:{}]",
builder.get(ES_HTTP_HOST_KEY), builder.get(ES_HTTP_PORT_KEY),
builder.put("path.logs", fileSystem.getLogDirectory().getAbsolutePath());
}
- private void configureAuthentication(Map<String, String> builder) {
+ private void configureSecurity(Map<String, String> builder) {
if (clusterEnabled && props.value((CLUSTER_SEARCH_PASSWORD.getKey())) != null) {
String clusterESKeystoreFileName = getFileNameFromPathProperty(CLUSTER_ES_KEYSTORE);
builder.put("xpack.security.transport.ssl.verification_mode", "certificate");
builder.put("xpack.security.transport.ssl.keystore.path", clusterESKeystoreFileName);
builder.put("xpack.security.transport.ssl.truststore.path", clusterESTruststoreFileName);
+
+ if (props.value(CLUSTER_ES_HTTP_KEYSTORE.getKey()) != null) {
+ String clusterESHttpKeystoreFileName = getFileNameFromPathProperty(CLUSTER_ES_HTTP_KEYSTORE);
+
+ builder.put("xpack.security.http.ssl.enabled", Boolean.TRUE.toString());
+ builder.put("xpack.security.http.ssl.keystore.path", clusterESHttpKeystoreFileName);
+ }
} else {
builder.put("xpack.security.autoconfiguration.enabled", Boolean.FALSE.toString());
builder.put("xpack.security.enabled", Boolean.FALSE.toString());
assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "keystore.pk12")).exists();
}
+ @Test
+ public void launch_whenEnablingEsWithHttpEncryption_shouldCopyKeystoreToEsConf() throws Exception {
+ File tempDir = temp.newFolder();
+ File certificateFile = temp.newFile("certificate.pk12");
+ File httpCertificateFile = temp.newFile("httpCertificate.pk12");
+ TestProcessBuilder processBuilder = new TestProcessBuilder();
+ ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+
+ EsInstallation esInstallation = createEsInstallation(new Props(new Properties())
+ .set("sonar.cluster.enabled", "true")
+ .set("sonar.cluster.search.password", "bootstrap-password")
+ .set("sonar.cluster.es.ssl.keystore", certificateFile.getAbsolutePath())
+ .set("sonar.cluster.es.ssl.truststore", certificateFile.getAbsolutePath())
+ .set("sonar.cluster.es.http.ssl.keystore", httpCertificateFile.getAbsolutePath())
+ .set("sonar.cluster.es.http.ssl.keystorePassword", "keystore-password"));
+
+ JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder());
+ command.addClasspath("lib/*.class");
+ command.addClasspath("lib/*.jar");
+ command.setArgument("foo", "bar");
+ command.setClassName("org.sonarqube.Main");
+ command.setEnvVariable("VAR1", "valueOfVar1");
+ command.setJvmOptions(new JvmOptions<>()
+ .add("-Dfoo=bar")
+ .add("-Dfoo2=bar2"));
+ command.setEsInstallation(esInstallation);
+
+ ManagedProcess monitor = underTest.launch(command);
+ assertThat(monitor).isNotNull();
+ assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "certificate.pk12")).exists();
+ assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "httpCertificate.pk12")).exists();
+ }
+
@Test
public void properties_are_passed_to_command_via_a_temporary_properties_file() throws Exception {
File tempDir = temp.newFolder();
import com.google.common.collect.Sets;
import com.google.common.net.HostAndPort;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.tls.HandshakeCertificates;
+import okhttp3.tls.HeldCertificate;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Rule
public MockWebServer mockWebServer = new MockWebServer();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
- EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())), null);
+ EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(),
+ mockWebServer.getPort())), null, null, null);
@After
public void after() {
mockServerResponse(200, JSON_SUCCESS_RESPONSE);
String password = "test-password";
- EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())), password);
+ EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(), mockWebServer.getPort())),
+ password, null, null);
assertThat(underTest.getClusterHealthStatus())
.hasValue(ClusterHealthStatus.YELLOW);
assertThat(mockWebServer.takeRequest().getHeader("Authorization")).isEqualTo("Basic ZWxhc3RpYzp0ZXN0LXBhc3N3b3Jk");
}
+ @Test
+ public void newInstance_whenKeyStorePassed_shouldCreateClient() throws GeneralSecurityException, IOException {
+ mockServerResponse(200, JSON_SUCCESS_RESPONSE);
+
+ Path keyStorePath = temp.newFile("keystore.p12").toPath();
+ String password = "password";
+
+ HandshakeCertificates certificate = createCertificate(mockWebServer.getHostName(), keyStorePath, password);
+ mockWebServer.useHttps(certificate.sslSocketFactory(), false);
+
+ EsConnectorImpl underTest = new EsConnectorImpl(Sets.newHashSet(HostAndPort.fromParts(mockWebServer.getHostName(),
+ mockWebServer.getPort())), null, keyStorePath, password);
+
+ assertThat(underTest.getClusterHealthStatus()).hasValue(ClusterHealthStatus.YELLOW);
+ }
+
+ private HandshakeCertificates createCertificate(String hostName, Path keyStorePath, String password)
+ throws GeneralSecurityException, IOException {
+ HeldCertificate localhostCertificate = new HeldCertificate.Builder()
+ .addSubjectAlternativeName(hostName)
+ .build();
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(null);
+ ks.setKeyEntry("alias", localhostCertificate.keyPair().getPrivate(), password.toCharArray(),
+ new java.security.cert.Certificate[]{localhostCertificate.certificate()});
+ ks.store(baos, password.toCharArray());
+
+ try (OutputStream outputStream = Files.newOutputStream(keyStorePath)) {
+ outputStream.write(baos.toByteArray());
+ }
+ }
+
+ return new HandshakeCertificates.Builder()
+ .heldCertificate(localhostCertificate)
+ .build();
+ }
+
private void mockServerResponse(int httpCode, String jsonResponse) {
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
import static org.sonar.process.ProcessProperties.Property.PATH_HOME;
import static org.sonar.process.ProcessProperties.Property.PATH_LOGS;
assertThat(underTest.getJvmOptions()).isEqualTo(new File(tempDir, "conf/es/jvm.options"));
}
+
+ @Test
+ public void isHttpEncryptionEnabled_shouldReturnCorrectValue() throws IOException {
+ File sqHomeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(PATH_DATA.getKey(), temp.newFolder().getAbsolutePath());
+ props.set(PATH_HOME.getKey(), temp.newFolder().getAbsolutePath());
+ props.set(PATH_TEMP.getKey(), sqHomeDir.getAbsolutePath());
+ props.set(PATH_LOGS.getKey(), temp.newFolder().getAbsolutePath());
+ props.set(CLUSTER_ENABLED.getKey(), "true");
+ props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "password");
+ props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), sqHomeDir.getAbsolutePath());
+
+ EsInstallation underTest = new EsInstallation(props);
+ assertThat(underTest.isHttpEncryptionEnabled()).isTrue();
+
+ props.set(CLUSTER_ENABLED.getKey(), "false");
+ props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), sqHomeDir.getAbsolutePath());
+
+ underTest = new EsInstallation(props);
+ assertThat(underTest.isHttpEncryptionEnabled()).isFalse();
+
+ props.set(CLUSTER_ENABLED.getKey(), "true");
+ props.rawProperties().remove(CLUSTER_ES_HTTP_KEYSTORE.getKey());
+
+ underTest = new EsInstallation(props);
+ assertThat(underTest.isHttpEncryptionEnabled()).isFalse();
+ }
}
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HOSTS;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
}
@Test
- public void configureAuthentication_givenClusterSearchPasswordNotProvided_dontAddXpackParameters() throws Exception {
+ public void configureSecurity_givenClusterSearchPasswordNotProvided_dontAddXpackParameters() throws Exception {
Props props = minProps(true);
EsSettings settings = new EsSettings(props, new EsInstallation(props), system);
}
@Test
- public void configureAuthentication_givenClusterSearchPasswordProvided_addXpackParameters_file_exists() throws Exception {
+ public void configureSecurity_givenClusterSearchPasswordProvided_addXpackParameters_file_exists() throws Exception {
Props props = minProps(true);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
File keystore = temp.newFile("keystore.p12");
}
@Test
- public void configureAuthentication_givenClusterSearchPasswordProvidedButKeystorePathMissing_throwException() throws Exception {
+ public void configureSecurity_givenClusterSearchPasswordProvidedButKeystorePathMissing_throwException() throws Exception {
Props props = minProps(true);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
}
@Test
- public void configureAuthentication_givenClusterModeFalse_dontAddXpackParameters() throws Exception {
+ public void configureSecurity_givenClusterModeFalse_dontAddXpackParameters() throws Exception {
Props props = minProps(false);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
}
@Test
- public void configureAuthentication_givenFileNotExist_throwException() throws Exception {
+ public void configureSecurity_givenFileNotExist_throwException() throws Exception {
Props props = minProps(true);
props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
File truststore = temp.newFile("truststore.p12");
.hasMessage("Unable to configure: sonar.cluster.es.ssl.keystore. File specified in [not-existing-file] does not exist");
}
+ @Test
+ public void configureSecurity_whenHttpKeystoreProvided_shouldAddHttpProperties() throws Exception {
+ Props props = minProps(true);
+ File keystore = temp.newFile("keystore.p12");
+ File truststore = temp.newFile("truststore.p12");
+ File httpKeystore = temp.newFile("http-keystore.p12");
+ props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
+ props.set(CLUSTER_ES_KEYSTORE.getKey(), keystore.getAbsolutePath());
+ props.set(CLUSTER_ES_TRUSTSTORE.getKey(), truststore.getAbsolutePath());
+ props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), httpKeystore.getAbsolutePath());
+
+ EsSettings settings = new EsSettings(props, new EsInstallation(props), system);
+
+ Map<String, String> outputParams = settings.build();
+
+ assertThat(outputParams)
+ .containsEntry("xpack.security.http.ssl.enabled", "true")
+ .containsEntry("xpack.security.http.ssl.keystore.path", httpKeystore.getName());
+ }
+
+ @Test
+ public void configureSecurity_whenHttpKeystoreNotProvided_shouldNotAddHttpProperties() throws Exception {
+ Props props = minProps(true);
+ File keystore = temp.newFile("keystore.p12");
+ File truststore = temp.newFile("truststore.p12");
+ props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
+ props.set(CLUSTER_ES_KEYSTORE.getKey(), keystore.getAbsolutePath());
+ props.set(CLUSTER_ES_TRUSTSTORE.getKey(), truststore.getAbsolutePath());
+
+ EsSettings settings = new EsSettings(props, new EsInstallation(props), system);
+
+ Map<String, String> outputParams = settings.build();
+
+ assertThat(outputParams)
+ .doesNotContainKey("xpack.security.http.ssl.enabled")
+ .doesNotContainKey("xpack.security.http.ssl.keystore.path");
+ }
+
+ @Test
+ public void configureSecurity_whenHttpKeystoreProvided_shouldFailIfNotExists() throws Exception {
+ Props props = minProps(true);
+ File keystore = temp.newFile("keystore.p12");
+ File truststore = temp.newFile("truststore.p12");
+ props.set(CLUSTER_SEARCH_PASSWORD.getKey(), "qwerty");
+ props.set(CLUSTER_ES_KEYSTORE.getKey(), keystore.getAbsolutePath());
+ props.set(CLUSTER_ES_TRUSTSTORE.getKey(), truststore.getAbsolutePath());
+ props.set(CLUSTER_ES_HTTP_KEYSTORE.getKey(), "not-existing-file");
+
+ EsSettings settings = new EsSettings(props, new EsInstallation(props), system);
+
+ assertThatThrownBy(settings::build)
+ .isInstanceOf(MessageException.class)
+ .hasMessage("Unable to configure: sonar.cluster.es.http.ssl.keystore. File specified in [not-existing-file] does not exist");
+ }
+
@DataProvider
public static Object[][] clusterEnabledOrNot() {
return new Object[][] {
CLUSTER_ES_TRUSTSTORE("sonar.cluster.es.ssl.truststore"),
CLUSTER_ES_KEYSTORE_PASSWORD("sonar.cluster.es.ssl.keystorePassword"),
CLUSTER_ES_TRUSTSTORE_PASSWORD("sonar.cluster.es.ssl.truststorePassword"),
+ CLUSTER_ES_HTTP_KEYSTORE("sonar.cluster.es.http.ssl.keystore"),
+ CLUSTER_ES_HTTP_KEYSTORE_PASSWORD("sonar.cluster.es.http.ssl.keystorePassword"),
// search node only settings
CLUSTER_ES_HOSTS("sonar.cluster.es.hosts"),
CLUSTER_ES_DISCOVERY_SEED_HOSTS("sonar.cluster.es.discovery.seed.hosts"),
testImplementation 'com.google.code.findbugs:jsr305'
testImplementation 'org.subethamail:subethasmtp'
testImplementation 'com.squareup.okhttp3:mockwebserver'
+ testImplementation 'com.squareup.okhttp3:okhttp-tls'
testImplementation 'com.squareup.okio:okio'
testImplementation 'com.tngtech.java:junit-dataprovider'
testImplementation 'junit:junit'
import com.google.gson.JsonObject;
import java.io.Closeable;
import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
import java.util.Arrays;
import java.util.function.Supplier;
import javax.annotation.Nullable;
+import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
-import org.elasticsearch.client.indices.GetMappingsRequest;
-import org.elasticsearch.client.indices.GetMappingsResponse;
-import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
+import org.elasticsearch.client.indices.GetMappingsRequest;
+import org.elasticsearch.client.indices.GetMappingsResponse;
+import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.common.Priority;
import org.jetbrains.annotations.NotNull;
private final Gson gson;
public EsClient(HttpHost... hosts) {
- this(new MinimalRestHighLevelClient(null, hosts));
+ this(new MinimalRestHighLevelClient(null, null, null, hosts));
}
- public EsClient(@Nullable String searchPassword, HttpHost... hosts) {
- this(new MinimalRestHighLevelClient(searchPassword, hosts));
+ public EsClient(@Nullable String searchPassword, @Nullable String keyStorePath, @Nullable String keyStorePassword, HttpHost... hosts) {
+ this(new MinimalRestHighLevelClient(searchPassword, keyStorePath, keyStorePassword, hosts));
}
EsClient(RestHighLevelClient restHighLevelClient) {
private static final int CONNECT_TIMEOUT = 5000;
private static final int SOCKET_TIMEOUT = 60000;
- public MinimalRestHighLevelClient(@Nullable String searchPassword, HttpHost... hosts) {
- super(buildHttpClient(searchPassword, hosts).build(), RestClient::close, Lists.newArrayList(), true);
+ public MinimalRestHighLevelClient(@Nullable String searchPassword, @Nullable String keyStorePath,
+ @Nullable String keyStorePassword, HttpHost... hosts) {
+ super(buildHttpClient(searchPassword, keyStorePath, keyStorePassword, hosts).build(), RestClient::close, Lists.newArrayList(), true);
}
MinimalRestHighLevelClient(RestClient restClient) {
}
@NotNull
- private static RestClientBuilder buildHttpClient(@Nullable String searchPassword,
- HttpHost[] hosts) {
+ private static RestClientBuilder buildHttpClient(@Nullable String searchPassword, @Nullable String keyStorePath,
+ @Nullable String keyStorePassword, HttpHost[] hosts) {
return RestClient.builder(hosts)
.setRequestConfigCallback(r -> r
.setConnectTimeout(CONNECT_TIMEOUT)
BasicCredentialsProvider provider = getBasicCredentialsProvider(searchPassword);
httpClientBuilder.setDefaultCredentialsProvider(provider);
}
+
+ if (keyStorePath != null) {
+ SSLContext sslContext = getSSLContext(keyStorePath, keyStorePassword);
+ httpClientBuilder.setSSLContext(sslContext);
+ }
+
return httpClientBuilder;
});
}
return provider;
}
+ private static SSLContext getSSLContext(String keyStorePath, @Nullable String keyStorePassword) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("pkcs12");
+ try (InputStream is = Files.newInputStream(Paths.get(keyStorePath))) {
+ keyStore.load(is, keyStorePassword == null ? null : keyStorePassword.toCharArray());
+ }
+ SSLContextBuilder sslBuilder = SSLContexts.custom().loadTrustMaterial(keyStore, null);
+ return sslBuilder.build();
+ } catch (IOException | GeneralSecurityException e) {
+ throw new IllegalStateException("Failed to setup SSL context on ES client", e);
+ }
+ }
}
<R> R execute(EsRequestExecutor<R> executor) {
import org.springframework.context.annotation.Bean;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE_PASSWORD;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_TYPE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_HOSTS;
// * in org.sonar.process.ProcessProperties.Property.SEARCH_HOST
// * in org.sonar.process.ProcessProperties.Property.SEARCH_PORT
HostAndPort host = HostAndPort.fromParts(config.get(SEARCH_HOST.getKey()).get(), config.getInt(SEARCH_PORT.getKey()).get());
- httpHosts = Collections.singletonList(toHttpHost(host));
+ httpHosts = Collections.singletonList(toHttpHost(host, config));
LOGGER.info("Connected to local Elasticsearch: [{}]", displayedAddresses(httpHosts));
}
- return new EsClient(config.get(CLUSTER_SEARCH_PASSWORD.getKey()).orElse(null), httpHosts.toArray(new HttpHost[0]));
+ return new EsClient(config.get(CLUSTER_SEARCH_PASSWORD.getKey()).orElse(null),
+ config.get(CLUSTER_ES_HTTP_KEYSTORE.getKey()).orElse(null),
+ config.get(CLUSTER_ES_HTTP_KEYSTORE_PASSWORD.getKey()).orElse(null),
+ httpHosts.toArray(new HttpHost[0]));
}
private static List<HttpHost> getHttpHosts(Configuration config) {
return Arrays.stream(config.getStringArray(CLUSTER_SEARCH_HOSTS.getKey()))
.map(HostAndPort::fromString)
- .map(EsClientProvider::toHttpHost)
+ .map(host -> toHttpHost(host, config))
.toList();
}
- private static HttpHost toHttpHost(HostAndPort host) {
+ private static HttpHost toHttpHost(HostAndPort host, Configuration config) {
try {
- return new HttpHost(InetAddress.getByName(host.getHost()), host.getPortOrDefault(9001));
+ String scheme = config.get(CLUSTER_ES_HTTP_KEYSTORE.getKey()).isPresent() ? "https" : HttpHost.DEFAULT_SCHEME_NAME;
+ return new HttpHost(InetAddress.getByName(host.getHost()), host.getPortOrDefault(9001), scheme);
} catch (UnknownHostException e) {
throw new IllegalStateException("Can not resolve host [" + host + "]", e);
}
*/
package org.sonar.server.es;
+import java.io.IOException;
import java.net.InetAddress;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
import org.assertj.core.api.Condition;
import org.elasticsearch.client.Node;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.slf4j.event.Level;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.testfixtures.log.LogTester;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_HTTP_KEYSTORE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NAME;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_NODE_TYPE;
import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_HOSTS;
public class EsClientProviderTest {
@Rule
public LogTester logTester = new LogTester();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
private MapSettings settings = new MapSettings();
private EsClientProvider underTest = new EsClientProvider();
assertThat(logTester.logs(Level.INFO))
.has(new Condition<>(s -> s.contains("Connected to remote Elasticsearch: [http://" + localhostHostname + ":9001, http://" + localhostHostname + ":8081]"), ""));
}
+
+ @Test
+ public void provide_whenHttpEncryptionEnabled_shouldUseHttps() throws GeneralSecurityException, IOException {
+ settings.setProperty(CLUSTER_ENABLED.getKey(), true);
+ Path keyStorePath = temp.newFile("keystore.p12").toPath();
+ EsClientTest.createCertificate("localhost", keyStorePath, "password");
+ settings.setProperty(CLUSTER_ES_HTTP_KEYSTORE.getKey(), keyStorePath.toString());
+ settings.setProperty(CLUSTER_NODE_TYPE.getKey(), "application");
+ settings.setProperty(CLUSTER_SEARCH_HOSTS.getKey(), format("%s,%s:8081", localhostHostname, localhostHostname));
+
+ EsClient client = underTest.provide(settings.asConfig());
+ RestHighLevelClient nativeClient = client.nativeClient();
+
+ Node node = nativeClient.getLowLevelClient().getNodes().get(0);
+ assertThat(node.getHost().getSchemeName()).isEqualTo("https");
+
+ assertThat(logTester.logs(Level.INFO))
+ .has(new Condition<>(s -> s.contains("Connected to remote Elasticsearch: [https://" + localhostHostname + ":9001, https://" + localhostHostname + ":8081]"), ""));
+ }
}
package org.sonar.server.es;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
import java.util.Objects;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.tls.HandshakeCertificates;
+import okhttp3.tls.HeldCertificate;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentMatcher;
import static org.assertj.core.api.Assertions.assertThat;
@Rule
public MockWebServer mockWebServer = new MockWebServer();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
RestClient restClient = mock(RestClient.class);
RestHighLevelClient client = new EsClient.MinimalRestHighLevelClient(restClient);
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_nodes/stats/fs,process,jvm,indices,breaker"))))
- .thenReturn(response);
+ .thenReturn(response);
assertThat(underTest.nodesStats()).isNotNull();
}
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_nodes/stats/fs,process,jvm,indices,breaker"))))
- .thenThrow(IOException.class);
+ .thenThrow(IOException.class);
assertThatThrownBy(() -> underTest.nodesStats())
.isInstanceOf(ElasticsearchException.class);
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_stats"))))
- .thenReturn(response);
+ .thenReturn(response);
assertThat(underTest.indicesStats()).isNotNull();
}
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_stats"))))
- .thenThrow(IOException.class);
+ .thenThrow(IOException.class);
assertThatThrownBy(() -> underTest.indicesStats())
.isInstanceOf(ElasticsearchException.class);
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_cluster/stats"))))
- .thenReturn(response);
+ .thenReturn(response);
assertThat(underTest.clusterStats()).isNotNull();
}
when(restClient.performRequest(argThat(new RawRequestMatcher(
"GET",
"/_cluster/stats"))))
- .thenThrow(IOException.class);
+ .thenThrow(IOException.class);
assertThatThrownBy(() -> underTest.clusterStats())
.isInstanceOf(ElasticsearchException.class);
.setHeader("Content-Type", "application/json"));
String password = "test-password";
- EsClient underTest = new EsClient(password, new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort()));
+ EsClient underTest = new EsClient(password, null, null, new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort()));
- underTest.clusterStats();
+ assertThat(underTest.clusterStats()).isNotNull();
assertThat(mockWebServer.takeRequest().getHeader("Authorization")).isEqualTo("Basic ZWxhc3RpYzp0ZXN0LXBhc3N3b3Jk");
}
+ @Test
+ public void newInstance_whenKeyStorePassed_shouldCreateClient() throws GeneralSecurityException, IOException {
+ mockWebServer.enqueue(new MockResponse()
+ .setResponseCode(200)
+ .setBody(EXAMPLE_CLUSTER_STATS_JSON)
+ .setHeader("Content-Type", "application/json"));
+
+ Path keyStorePath = temp.newFile("keystore.p12").toPath();
+ String password = "password";
+
+ HandshakeCertificates certificate = createCertificate(mockWebServer.getHostName(), keyStorePath, password);
+ mockWebServer.useHttps(certificate.sslSocketFactory(), false);
+
+ EsClient underTest = new EsClient(null, keyStorePath.toString(), password,
+ new HttpHost(mockWebServer.getHostName(), mockWebServer.getPort(), "https"));
+
+ assertThat(underTest.clusterStats()).isNotNull();
+ }
+
+ static HandshakeCertificates createCertificate(String hostName, Path keyStorePath, String password) throws GeneralSecurityException, IOException {
+ HeldCertificate localhostCertificate = new HeldCertificate.Builder()
+ .addSubjectAlternativeName(hostName)
+ .build();
+
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(null);
+ ks.setKeyEntry("alias", localhostCertificate.keyPair().getPrivate(), password.toCharArray(),
+ new java.security.cert.Certificate[]{localhostCertificate.certificate()});
+ ks.store(baos, password.toCharArray());
+
+ try (OutputStream outputStream = Files.newOutputStream(keyStorePath)) {
+ outputStream.write(baos.toByteArray());
+ }
+ }
+
+ return new HandshakeCertificates.Builder()
+ .heldCertificate(localhostCertificate)
+ .build();
+ }
+
static class RawRequestMatcher implements ArgumentMatcher<Request> {
String endpoint;
String method;