Skip to content

Commit 4b5b34a

Browse files
pioorgeddumelendez
andauthored
Enable lazy certificates for Elasticsearch (#7991)
Co-authored-by: Eddú Meléndez Gonzales <eddu.melendez@gmail.com>
1 parent 1846805 commit 4b5b34a

File tree

3 files changed

+83
-27
lines changed

3 files changed

+83
-27
lines changed

modules/elasticsearch/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ dependencies {
66
testImplementation "org.elasticsearch.client:transport:7.17.17"
77
testImplementation 'org.assertj:assertj-core:3.25.2'
88
}
9+
10+
tasks.japicmp {
11+
methodExcludes = [
12+
"org.testcontainers.elasticsearch.ElasticsearchContainer#containerIsStarted(com.github.dockerjava.api.command.InspectContainerResponse)",
13+
]
14+
}

modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.testcontainers.elasticsearch;
22

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

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

68+
// default location of the automatically generated self-signed HTTP cert for versions >= 8
69+
private static final String DEFAULT_CERT_PATH = "/usr/share/elasticsearch/config/certs/http_ca.crt";
70+
6971
/**
7072
* Elasticsearch Default version
7173
*/
@@ -77,9 +79,7 @@ public class ElasticsearchContainer extends GenericContainer<ElasticsearchContai
7779

7880
private final boolean isAtLeastMajorVersion8;
7981

80-
private Optional<byte[]> caCertAsBytes = Optional.empty();
81-
82-
private String certPath = "/usr/share/elasticsearch/config/certs/http_ca.crt";
82+
private String certPath = "";
8383

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

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

100101
/**
101102
* Create an Elasticsearch Container by passing the full docker image name
103+
*
102104
* @param dockerImageName Full docker image name as a {@link DockerImageName}, like: DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.9.2")
103105
*/
104106
public ElasticsearchContainer(final DockerImageName dockerImageName) {
@@ -136,23 +138,7 @@ public ElasticsearchContainer(final DockerImageName dockerImageName) {
136138
setWaitStrategy(new LogMessageWaitStrategy().withRegEx(regex));
137139
if (isAtLeastMajorVersion8) {
138140
withPassword(ELASTICSEARCH_DEFAULT_PASSWORD);
139-
}
140-
}
141-
142-
@Override
143-
protected void containerIsStarted(InspectContainerResponse containerInfo) {
144-
if (isAtLeastMajorVersion8 && StringUtils.isNotEmpty(certPath)) {
145-
try {
146-
byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray);
147-
if (bytes.length > 0) {
148-
this.caCertAsBytes = Optional.of(bytes);
149-
}
150-
} catch (NotFoundException e) {
151-
// just emit an error message, but do not throw an exception
152-
// this might be ok, if the docker image is accidentally looking like version 8 or latest
153-
// can happen if Elasticsearch is repackaged, i.e. with custom plugins
154-
log.warn("CA cert under " + certPath + " not found.");
155-
}
141+
withCertPath(DEFAULT_CERT_PATH);
156142
}
157143
}
158144

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

168168
/**
169-
* A SSL context based on the self signed CA, so that using this SSL Context allows to connect to the Elasticsearch service
169+
* A SSL context based on the self-signed CA, so that using this SSL Context allows to connect to the Elasticsearch service
170170
* @return a customized SSL Context
171171
*/
172172
public SSLContext createSslContextFromCa() {
173173
try {
174174
CertificateFactory factory = CertificateFactory.getInstance("X.509");
175-
Certificate trustedCa = factory.generateCertificate(new ByteArrayInputStream(caCertAsBytes.get()));
175+
Certificate trustedCa = factory.generateCertificate(
176+
new ByteArrayInputStream(
177+
caCertAsBytes()
178+
.orElseThrow(() -> new IllegalStateException("CA cert under " + certPath + " not found."))
179+
)
180+
);
176181
KeyStore trustStore = KeyStore.getInstance("pkcs12");
177182
trustStore.load(null, null);
178183
trustStore.setCertificateEntry("ca", trustedCa);
@@ -190,13 +195,13 @@ public SSLContext createSslContextFromCa() {
190195
/**
191196
* Define the Elasticsearch password to set. It enables security behind the scene for major version below 8.0.0.
192197
* It's not possible to use security with the oss image.
193-
* @param password Password to set
198+
* @param password Password to set
194199
* @return this
195200
*/
196201
public ElasticsearchContainer withPassword(String password) {
197202
if (isOss) {
198203
throw new IllegalArgumentException(
199-
"You can not activate security on Elastic OSS Image. " + "Please switch to the default distribution"
204+
"You can not activate security on Elastic OSS Image. Please switch to the default distribution"
200205
);
201206
}
202207
withEnv("ELASTIC_PASSWORD", password);
@@ -222,7 +227,8 @@ public String getHttpHostAddress() {
222227
return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT);
223228
}
224229

225-
@Deprecated // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future.
230+
// The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future.
231+
@Deprecated
226232
public InetSocketAddress getTcpHost() {
227233
return new InetSocketAddress(getHost(), getMappedPort(ELASTICSEARCH_DEFAULT_TCP_PORT));
228234
}

modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
2424
import org.testcontainers.containers.wait.strategy.Wait;
2525
import org.testcontainers.images.RemoteDockerImage;
26+
import org.testcontainers.images.builder.Transferable;
2627
import org.testcontainers.utility.DockerImageName;
2728
import org.testcontainers.utility.MountableFile;
2829

@@ -375,6 +376,49 @@ public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throw
375376
}
376377
}
377378

379+
@Test
380+
public void testElasticsearch7CanHaveSecurityEnabledAndUseSslContext() throws Exception {
381+
String customizedCertPath = "/usr/share/elasticsearch/config/certs/http_ca_customized.crt";
382+
try (
383+
ElasticsearchContainer container = new ElasticsearchContainer(
384+
"docker.elastic.co/elasticsearch/elasticsearch:7.17.15"
385+
)
386+
.withPassword(ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
387+
.withEnv("xpack.security.enabled", "true")
388+
.withEnv("xpack.security.http.ssl.enabled", "true")
389+
.withEnv("xpack.security.http.ssl.key", "/usr/share/elasticsearch/config/certs/elasticsearch.key")
390+
.withEnv(
391+
"xpack.security.http.ssl.certificate",
392+
"/usr/share/elasticsearch/config/certs/elasticsearch.crt"
393+
)
394+
.withEnv("xpack.security.http.ssl.certificate_authorities", customizedCertPath)
395+
// these lines show how certificates can be created self-made way
396+
// obviously this shouldn't be done in prod environment, where proper and officially signed keys should be present
397+
.withCopyToContainer(
398+
Transferable.of(
399+
"#!/bin/bash\n" +
400+
"mkdir -p /usr/share/elasticsearch/config/certs;" +
401+
"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\";" +
402+
"openssl x509 -outform der -in /usr/share/elasticsearch/config/certs/elasticsearch.crt -out " +
403+
customizedCertPath +
404+
"; chown -R elasticsearch /usr/share/elasticsearch/config/certs/",
405+
555
406+
),
407+
"/usr/share/elasticsearch/generate-certs.sh"
408+
)
409+
// because we need to generate the certificates before Elasticsearch starts, the entry command has to be tuned accordingly
410+
.withCommand(
411+
"sh",
412+
"-c",
413+
"/usr/share/elasticsearch/generate-certs.sh && /usr/local/bin/docker-entrypoint.sh"
414+
)
415+
.withCertPath(customizedCertPath)
416+
) {
417+
container.start();
418+
assertClusterHealthResponse(container);
419+
}
420+
}
421+
378422
@Test
379423
public void testElasticsearchDefaultMaxHeapSize() throws Exception {
380424
long defaultHeapSize = 2147483648L;

0 commit comments

Comments
 (0)