Skip to content

Commit dcc9cf0

Browse files
authored
Add SSL setup helper class and documentation (#390)
1 parent 01320ec commit dcc9cf0

File tree

5 files changed

+419
-4
lines changed

5 files changed

+419
-4
lines changed

docs/getting-started/connecting.asciidoc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,74 @@ concise DSL-like code. This pattern is explained in more detail in
3838
include-tagged::{doc-tests-src}/getting_started/ConnectingTest.java[first-request]
3939
--------------------------------------------------
4040

41+
[discrete]
42+
==== Using a secure connection
43+
44+
The <<java-rest-low>> documentation explains how to set up encrypted communications in detail.
45+
46+
In self-managed installations, Elasticsearch will start with security features like authentication and TLS enabled. To connect to the Elasticsearch cluster you’ll need to configure the {java-client} to use HTTPS with the generated CA certificate in order to make requests successfully.
47+
48+
When you start Elasticsearch for the first time you’ll see a distinct block like the one below in the output from Elasticsearch (you may have to scroll up if it’s been a while):
49+
50+
["source","xml"]
51+
----------------------------------------------------------------
52+
-> Elasticsearch security features have been automatically configured!
53+
-> Authentication is enabled and cluster connections are encrypted.
54+
55+
-> Password for the elastic user (reset with `bin/elasticsearch-reset-password -u elastic`):
56+
lhQpLELkjkrawaBoaz0Q
57+
58+
-> HTTP CA certificate SHA-256 fingerprint:
59+
a52dd93511e8c6045e21f16654b77c9ee0f34aea26d9f40320b531c474676228
60+
...
61+
----------------------------------------------------------------
62+
63+
Note down the elastic user password and HTTP CA fingerprint for the next sections. In the examples below they will be stored in the variables `password` and `fingerprint` respectively.
64+
65+
Depending on the context, you have two options for verifying the HTTPS connection: either verifying with the CA certificate itself or using the CA certificate fingerprint. For both cases, the {java-client}'s `TransportUtils` class provides convenience methods to easily create an `SSLContext`.
66+
67+
[discrete]
68+
===== Verifying HTTPS with a certificate fingerprint
69+
70+
This method of verifying the HTTPS connection uses the certificate fingerprint value noted down earlier.
71+
72+
["source","java"]
73+
--------------------------------------------------
74+
include-tagged::{doc-tests-src}/getting_started/ConnectingTest.java[create-secure-client-fingerprint]
75+
--------------------------------------------------
76+
<1> Create an `SSLContext` with the certificate fingerprint.
77+
<2> Set up authentication.
78+
<3> Do not forget to set the protocol to `https`!
79+
<4> Configure the http client with the SSL and authentication configurations.
80+
81+
Note that the certificate fingerprint can also be calculated using `openssl x509` with the certificate file:
82+
["source","bash"]
83+
--------------------------------------------------
84+
openssl x509 -fingerprint -sha256 -noout -in /path/to/http_ca.crt
85+
--------------------------------------------------
86+
87+
If you don’t have access to the generated CA file from Elasticsearch you can use the following script to output the root CA fingerprint of the Elasticsearch instance with `openssl s_client`:
88+
89+
["source","bash"]
90+
--------------------------------------------------
91+
openssl s_client -connect localhost:9200 -servername localhost -showcerts </dev/null 2>/dev/null \
92+
| openssl x509 -fingerprint -sha256 -noout -in /dev/stdin
93+
--------------------------------------------------
94+
95+
[discrete]
96+
===== Verifying HTTPS with a CA certificate
97+
98+
The generated root CA certificate can be found in the `certs` directory in your Elasticsearch config location. If you’re running Elasticsearch in Docker there is {es-docs}/docker.html[additional documentation] for retrieving the CA certificate.
99+
100+
Once you have made the `http_ca.crt` file available to your application, you can use it to set up the client:
101+
102+
["source","java"]
103+
--------------------------------------------------
104+
include-tagged::{doc-tests-src}/getting_started/ConnectingTest.java[create-secure-client-cert]
105+
--------------------------------------------------
106+
<1> Create an `SSLContext` with the `http_ca.crt` file.
107+
<2> Set up authentication.
108+
<3> Do not forget to set the protocol to `https`!
109+
<4> Configure the http client with the SSL and authentication configurations.
110+
41111
{doc-tests-blurb}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.transport;
21+
22+
import javax.net.ssl.SSLContext;
23+
import javax.net.ssl.TrustManagerFactory;
24+
import javax.net.ssl.X509TrustManager;
25+
import java.io.File;
26+
import java.io.FileInputStream;
27+
import java.io.IOException;
28+
import java.io.InputStream;
29+
import java.security.KeyManagementException;
30+
import java.security.KeyStore;
31+
import java.security.KeyStoreException;
32+
import java.security.MessageDigest;
33+
import java.security.NoSuchAlgorithmException;
34+
import java.security.cert.Certificate;
35+
import java.security.cert.CertificateException;
36+
import java.security.cert.CertificateFactory;
37+
import java.security.cert.X509Certificate;
38+
import java.util.Arrays;
39+
40+
public class TransportUtils {
41+
42+
/**
43+
* Creates an <code>SSLContext</code> from the self-signed <code>http_ca.crt</code> certificate created by Elasticsearch during
44+
* its first start.
45+
*
46+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-stack-security.html">Elasticsearch
47+
* documentation</a>
48+
*/
49+
public static SSLContext sslContextFromHttpCaCrt(File file) throws IOException {
50+
try(InputStream in = new FileInputStream(file)) {
51+
return sslContextFromHttpCaCrt(in);
52+
}
53+
}
54+
55+
/**
56+
* Creates an <code>SSLContext</code> from the self-signed <code>http_ca.crt</code> certificate created by Elasticsearch during
57+
* its first start.
58+
*
59+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-stack-security.html">Elasticsearch
60+
* documentation</a>
61+
*/
62+
public static SSLContext sslContextFromHttpCaCrt(InputStream in) {
63+
try {
64+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
65+
Certificate certificate = cf.generateCertificate(in);
66+
67+
final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
68+
keyStore.load(null, null);
69+
keyStore.setCertificateEntry("elasticsearch-ca", certificate);
70+
71+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
72+
tmf.init(keyStore);
73+
74+
SSLContext sslContext = SSLContext.getInstance("TLS");
75+
sslContext.init(null, tmf.getTrustManagers(), null);
76+
return sslContext;
77+
78+
} catch (CertificateException | NoSuchAlgorithmException | KeyManagementException | KeyStoreException | IOException e) {
79+
throw new RuntimeException(e);
80+
}
81+
}
82+
83+
/**
84+
* Creates an <code>SSLContext</code> from the SHA-256 fingerprint of self-signed <code>http_ca.crt</code> certificate output by
85+
* Elasticsearch at startup time.
86+
*
87+
* @param fingerPrint the SHA-256 fingerprint. Can be uppercase or lowercase, with or without colons separating bytes
88+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-stack-security.html">Elasticsearch
89+
* documentation</a>
90+
*/
91+
public static SSLContext sslContextFromCaFingerprint(String fingerPrint) {
92+
93+
fingerPrint = fingerPrint.replace(":", "");
94+
int len = fingerPrint.length();
95+
byte[] fpBytes = new byte[len / 2];
96+
for (int i = 0; i < len; i += 2) {
97+
fpBytes[i / 2] = (byte) (
98+
(Character.digit(fingerPrint.charAt(i), 16) << 4) +
99+
Character.digit(fingerPrint.charAt(i+1), 16)
100+
);
101+
}
102+
103+
try {
104+
X509TrustManager tm = new X509TrustManager() {
105+
@Override
106+
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
107+
throw new CertificateException("This is a client-side only trust manager");
108+
}
109+
110+
@Override
111+
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
112+
113+
// The CA root is the last element of the chain
114+
X509Certificate anchor = chain[chain.length - 1];
115+
116+
byte[] bytes;
117+
try {
118+
MessageDigest md = MessageDigest.getInstance("SHA-256");
119+
md.update(anchor.getEncoded());
120+
bytes = md.digest();
121+
} catch (NoSuchAlgorithmException e) {
122+
throw new RuntimeException(e);
123+
}
124+
125+
if (Arrays.equals(fpBytes, bytes)) {
126+
return;
127+
}
128+
129+
throw new CertificateException("Untrusted certificate: " + anchor.getSubjectX500Principal());
130+
}
131+
132+
@Override
133+
public X509Certificate[] getAcceptedIssuers() {
134+
return new X509Certificate[0];
135+
}
136+
};
137+
138+
SSLContext sslContext = SSLContext.getInstance("TLS");
139+
sslContext.init(null, new X509TrustManager[] { tm }, null);
140+
return sslContext;
141+
142+
} catch (NoSuchAlgorithmException | KeyManagementException e) {
143+
// Exceptions that should normally not occur
144+
throw new RuntimeException(e);
145+
}
146+
}
147+
}

java-client/src/test/java/co/elastic/clients/documentation/getting_started/ConnectingTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,19 @@
2525
import co.elastic.clients.elasticsearch.core.search.Hit;
2626
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
2727
import co.elastic.clients.transport.ElasticsearchTransport;
28+
import co.elastic.clients.transport.TransportUtils;
2829
import co.elastic.clients.transport.rest_client.RestClientTransport;
2930
import org.apache.http.HttpHost;
31+
import org.apache.http.auth.AuthScope;
32+
import org.apache.http.auth.UsernamePasswordCredentials;
33+
import org.apache.http.impl.client.BasicCredentialsProvider;
3034
import org.elasticsearch.client.RestClient;
3135
import org.junit.jupiter.api.Disabled;
3236
import org.junit.jupiter.api.Test;
3337

38+
import javax.net.ssl.SSLContext;
39+
import java.io.File;
40+
3441
public class ConnectingTest {
3542

3643
@Disabled // we don't have a running ES
@@ -65,6 +72,76 @@ public void createClient() throws Exception {
6572
//end::first-request
6673
}
6774

75+
@Disabled // we don't have a running ES
76+
@Test
77+
public void createSecureClientCert() throws Exception {
78+
79+
// Create the low-level client
80+
String host = "localhost";
81+
int port = 9200;
82+
String login = "elastic";
83+
String password = "changeme";
84+
85+
//tag::create-secure-client-cert
86+
File certFile = new File("/path/to/http_ca.crt");
87+
88+
SSLContext sslContext = TransportUtils
89+
.sslContextFromHttpCaCrt(certFile); // <1>
90+
91+
BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); // <2>
92+
credsProv.setCredentials(
93+
AuthScope.ANY, new UsernamePasswordCredentials(login, password)
94+
);
95+
96+
RestClient restClient = RestClient
97+
.builder(new HttpHost(host, port, "https")) // <3>
98+
.setHttpClientConfigCallback(hc -> hc
99+
.setSSLContext(sslContext) // <4>
100+
.setDefaultCredentialsProvider(credsProv)
101+
)
102+
.build();
103+
104+
// Create the transport and the API client
105+
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
106+
ElasticsearchClient client = new ElasticsearchClient(transport);
107+
//end::create-secure-client-cert
108+
}
109+
110+
@Disabled // we don't have a running ES
111+
@Test
112+
public void createSecureClientFingerPrint() throws Exception {
113+
114+
// Create the low-level client
115+
String host = "localhost";
116+
int port = 9200;
117+
String login = "elastic";
118+
String password = "changeme";
119+
120+
//tag::create-secure-client-fingerprint
121+
String fingerprint = "<certificate fingerprint>";
122+
123+
SSLContext sslContext = TransportUtils
124+
.sslContextFromCaFingerprint(fingerprint); // <1>
125+
126+
BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); // <2>
127+
credsProv.setCredentials(
128+
AuthScope.ANY, new UsernamePasswordCredentials(login, password)
129+
);
130+
131+
RestClient restClient = RestClient
132+
.builder(new HttpHost(host, port, "https")) // <3>
133+
.setHttpClientConfigCallback(hc -> hc
134+
.setSSLContext(sslContext) // <4>
135+
.setDefaultCredentialsProvider(credsProv)
136+
)
137+
.build();
138+
139+
// Create the transport and the API client
140+
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
141+
ElasticsearchClient client = new ElasticsearchClient(transport);
142+
//end::create-secure-client-fingerprint
143+
}
144+
68145
private void processProduct(Product p) {}
69146

70147
}

java-client/src/test/java/co/elastic/clients/elasticsearch/ElasticsearchTestServer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ public class ElasticsearchTestServer implements AutoCloseable {
4646

4747
private final String[] plugins;
4848
private volatile ElasticsearchContainer container;
49-
private int port;
5049
private final JsonpMapper mapper = new JsonbJsonpMapper();
5150
private RestClient restClient;
5251
private ElasticsearchTransport transport;
@@ -102,7 +101,7 @@ public synchronized ElasticsearchTestServer start() {
102101
.withPassword("changeme");
103102
container.start();
104103

105-
port = container.getMappedPort(9200);
104+
int port = container.getMappedPort(9200);
106105

107106
boolean useTLS = version.major() >= 8;
108107
HttpHost host = new HttpHost("localhost", port, useTLS ? "https": "http");
@@ -165,8 +164,8 @@ public void close() {
165164
container = null;
166165
}
167166

168-
public int port() {
169-
return port;
167+
public ElasticsearchContainer container() {
168+
return this.container;
170169
}
171170

172171
public RestClient restClient() {

0 commit comments

Comments
 (0)