Skip to content

Commit

Permalink
Enable lazy certificates for Elasticsearch (#7991)
Browse files Browse the repository at this point in the history
Co-authored-by: Eddú Meléndez Gonzales <[email protected]>
  • Loading branch information
pioorg and eddumelendez authored Mar 4, 2024
1 parent 1846805 commit 4b5b34a
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 27 deletions.
6 changes: 6 additions & 0 deletions modules/elasticsearch/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ dependencies {
testImplementation "org.elasticsearch.client:transport:7.17.17"
testImplementation 'org.assertj:assertj-core:3.25.2'
}

tasks.japicmp {
methodExcludes = [
"org.testcontainers.elasticsearch.ElasticsearchContainer#containerIsStarted(com.github.dockerjava.api.command.InspectContainerResponse)",
]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.testcontainers.elasticsearch;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
Expand Down Expand Up @@ -66,6 +65,9 @@ public class ElasticsearchContainer extends GenericContainer<ElasticsearchContai

private static final DockerImageName ELASTICSEARCH_IMAGE_NAME = DockerImageName.parse("elasticsearch");

// default location of the automatically generated self-signed HTTP cert for versions >= 8
private static final String DEFAULT_CERT_PATH = "/usr/share/elasticsearch/config/certs/http_ca.crt";

/**
* Elasticsearch Default version
*/
Expand All @@ -77,9 +79,7 @@ public class ElasticsearchContainer extends GenericContainer<ElasticsearchContai

private final boolean isAtLeastMajorVersion8;

private Optional<byte[]> caCertAsBytes = Optional.empty();

private String certPath = "/usr/share/elasticsearch/config/certs/http_ca.crt";
private String certPath = "";

/**
* @deprecated use {@link #ElasticsearchContainer(DockerImageName)} instead
Expand All @@ -91,6 +91,7 @@ public ElasticsearchContainer() {

/**
* Create an Elasticsearch Container by passing the full docker image name
*
* @param dockerImageName Full docker image name as a {@link String}, like: docker.elastic.co/elasticsearch/elasticsearch:7.9.2
*/
public ElasticsearchContainer(String dockerImageName) {
Expand All @@ -99,6 +100,7 @@ public ElasticsearchContainer(String dockerImageName) {

/**
* Create an Elasticsearch Container by passing the full docker image name
*
* @param dockerImageName Full docker image name as a {@link DockerImageName}, like: DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.9.2")
*/
public ElasticsearchContainer(final DockerImageName dockerImageName) {
Expand Down Expand Up @@ -136,23 +138,7 @@ public ElasticsearchContainer(final DockerImageName dockerImageName) {
setWaitStrategy(new LogMessageWaitStrategy().withRegEx(regex));
if (isAtLeastMajorVersion8) {
withPassword(ELASTICSEARCH_DEFAULT_PASSWORD);
}
}

@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
if (isAtLeastMajorVersion8 && StringUtils.isNotEmpty(certPath)) {
try {
byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray);
if (bytes.length > 0) {
this.caCertAsBytes = Optional.of(bytes);
}
} catch (NotFoundException e) {
// just emit an error message, but do not throw an exception
// this might be ok, if the docker image is accidentally looking like version 8 or latest
// can happen if Elasticsearch is repackaged, i.e. with custom plugins
log.warn("CA cert under " + certPath + " not found.");
}
withCertPath(DEFAULT_CERT_PATH);
}
}

Expand All @@ -162,17 +148,36 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) {
* @return byte array optional containing the CA cert extracted from the docker container
*/
public Optional<byte[]> caCertAsBytes() {
return caCertAsBytes;
if (StringUtils.isBlank(certPath)) {
return Optional.empty();
}
try {
byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray);
if (bytes.length > 0) {
return Optional.of(bytes);
}
} catch (NotFoundException e) {
// just emit an error message, but do not throw an exception
// this might be ok, if the docker image is accidentally looking like version 8 or latest
// can happen if Elasticsearch is repackaged, i.e. with custom plugins
log.warn("CA cert under " + certPath + " not found.");
}
return Optional.empty();
}

/**
* A SSL context based on the self signed CA, so that using this SSL Context allows to connect to the Elasticsearch service
* A SSL context based on the self-signed CA, so that using this SSL Context allows to connect to the Elasticsearch service
* @return a customized SSL Context
*/
public SSLContext createSslContextFromCa() {
try {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
Certificate trustedCa = factory.generateCertificate(new ByteArrayInputStream(caCertAsBytes.get()));
Certificate trustedCa = factory.generateCertificate(
new ByteArrayInputStream(
caCertAsBytes()
.orElseThrow(() -> new IllegalStateException("CA cert under " + certPath + " not found."))
)
);
KeyStore trustStore = KeyStore.getInstance("pkcs12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", trustedCa);
Expand All @@ -190,13 +195,13 @@ public SSLContext createSslContextFromCa() {
/**
* Define the Elasticsearch password to set. It enables security behind the scene for major version below 8.0.0.
* It's not possible to use security with the oss image.
* @param password Password to set
* @param password Password to set
* @return this
*/
public ElasticsearchContainer withPassword(String password) {
if (isOss) {
throw new IllegalArgumentException(
"You can not activate security on Elastic OSS Image. " + "Please switch to the default distribution"
"You can not activate security on Elastic OSS Image. Please switch to the default distribution"
);
}
withEnv("ELASTIC_PASSWORD", password);
Expand All @@ -222,7 +227,8 @@ public String getHttpHostAddress() {
return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT);
}

@Deprecated // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future.
// The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future.
@Deprecated
public InetSocketAddress getTcpHost() {
return new InetSocketAddress(getHost(), getMappedPort(ELASTICSEARCH_DEFAULT_TCP_PORT));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.RemoteDockerImage;
import org.testcontainers.images.builder.Transferable;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;

Expand Down Expand Up @@ -375,6 +376,49 @@ public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throw
}
}

@Test
public void testElasticsearch7CanHaveSecurityEnabledAndUseSslContext() throws Exception {
String customizedCertPath = "/usr/share/elasticsearch/config/certs/http_ca_customized.crt";
try (
ElasticsearchContainer container = new ElasticsearchContainer(
"docker.elastic.co/elasticsearch/elasticsearch:7.17.15"
)
.withPassword(ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
.withEnv("xpack.security.enabled", "true")
.withEnv("xpack.security.http.ssl.enabled", "true")
.withEnv("xpack.security.http.ssl.key", "/usr/share/elasticsearch/config/certs/elasticsearch.key")
.withEnv(
"xpack.security.http.ssl.certificate",
"/usr/share/elasticsearch/config/certs/elasticsearch.crt"
)
.withEnv("xpack.security.http.ssl.certificate_authorities", customizedCertPath)
// these lines show how certificates can be created self-made way
// obviously this shouldn't be done in prod environment, where proper and officially signed keys should be present
.withCopyToContainer(
Transferable.of(
"#!/bin/bash\n" +
"mkdir -p /usr/share/elasticsearch/config/certs;" +
"openssl req -x509 -newkey rsa:4096 -keyout /usr/share/elasticsearch/config/certs/elasticsearch.key -out /usr/share/elasticsearch/config/certs/elasticsearch.crt -days 365 -nodes -subj \"/CN=localhost\";" +
"openssl x509 -outform der -in /usr/share/elasticsearch/config/certs/elasticsearch.crt -out " +
customizedCertPath +
"; chown -R elasticsearch /usr/share/elasticsearch/config/certs/",
555
),
"/usr/share/elasticsearch/generate-certs.sh"
)
// because we need to generate the certificates before Elasticsearch starts, the entry command has to be tuned accordingly
.withCommand(
"sh",
"-c",
"/usr/share/elasticsearch/generate-certs.sh && /usr/local/bin/docker-entrypoint.sh"
)
.withCertPath(customizedCertPath)
) {
container.start();
assertClusterHealthResponse(container);
}
}

@Test
public void testElasticsearchDefaultMaxHeapSize() throws Exception {
long defaultHeapSize = 2147483648L;
Expand Down

0 comments on commit 4b5b34a

Please sign in to comment.