Skip to content

Commit 2c1a01d

Browse files
authored
Merge pull request #2874 from ClickHouse/06/10/26/ssl_modes
[client-v2, jdbc-v2] Implement SSL Modes
2 parents 2916f47 + 8babe73 commit 2c1a01d

17 files changed

Lines changed: 1031 additions & 58 deletions

File tree

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.clickhouse.client.api.data_formats.internal.ProcessParser;
1313
import com.clickhouse.client.api.enums.Protocol;
1414
import com.clickhouse.client.api.enums.ProxyType;
15+
import com.clickhouse.client.api.enums.SSLMode;
1516
import com.clickhouse.client.api.http.ClickHouseHttpProto;
1617
import com.clickhouse.client.api.insert.InsertResponse;
1718
import com.clickhouse.client.api.insert.InsertSettings;
@@ -755,6 +756,34 @@ public Builder setClientKey(String path) {
755756
return this;
756757
}
757758

759+
/**
760+
* Defines how strictly the client verifies a server identity on secure connections.
761+
*
762+
* <p>Supported modes:</p>
763+
* <ul>
764+
* <li>{@link SSLMode#DISABLED} - SSL is not used; only meaningful with plain protocols</li>
765+
* <li>{@link SSLMode#TRUST} - encrypt, but accept any server certificate and skip
766+
* hostname verification; a configured trust store or CA certificate is ignored (a warning
767+
* is logged), while a client certificate/key is still applied for mTLS</li>
768+
* <li>{@link SSLMode#VERIFY_CA} - validate the server certificate chain, but skip
769+
* hostname verification</li>
770+
* <li>{@link SSLMode#STRICT} - full verification of the certificate chain and the
771+
* hostname (default)</li>
772+
* </ul>
773+
*
774+
* <p>The mode applies only when a secure protocol is in use - for the HTTP transport that
775+
* means an {@code https://} endpoint. Setting any mode does <b>not</b> make the client use
776+
* encryption on a plain HTTP endpoint: the endpoint scheme always decides whether the
777+
* connection is encrypted.</p>
778+
*
779+
* @param sslMode ssl mode
780+
* @return same instance of the builder
781+
*/
782+
public Builder setSSLMode(SSLMode sslMode) {
783+
this.configuration.put(ClientConfigProperties.SSL_MODE.getKey(), sslMode.name());
784+
return this;
785+
}
786+
758787
/**
759788
* Configure client to use server timezone for date/datetime columns. Default is true.
760789
* If this options is selected then server timezone should be set as well.
@@ -1140,6 +1169,36 @@ public Client build() {
11401169
throw new ClientMisconfigurationException("Trust store and certificates cannot be used together");
11411170
}
11421171

1172+
// A trust store and a CA certificate are not rejected here: for VERIFY_CA/STRICT the trust
1173+
// store takes precedence and the CA certificate is ignored with a warning (see createSSLContext).
1174+
1175+
// Resolve ssl_mode case-insensitively and normalize it to the canonical enum name so that
1176+
// downstream parsing is consistent and an unknown value is reported as a misconfiguration
1177+
// here instead of failing later with a generic enum-parsing error.
1178+
String sslModeValue = configuration.get(ClientConfigProperties.SSL_MODE.getKey());
1179+
if (sslModeValue != null) {
1180+
SSLMode sslMode;
1181+
try {
1182+
sslMode = SSLMode.fromValue(sslModeValue);
1183+
} catch (IllegalArgumentException e) {
1184+
throw new ClientMisconfigurationException("Invalid value '" + sslModeValue + "' for '"
1185+
+ ClientConfigProperties.SSL_MODE.getKey() + "'", e);
1186+
}
1187+
configuration.put(ClientConfigProperties.SSL_MODE.getKey(), sslMode.name());
1188+
1189+
// SSLMode.DISABLED does not turn encryption off - the endpoint scheme decides that. So it
1190+
// contradicts a secure (https) endpoint and must be rejected here, before the client is created.
1191+
if (sslMode == SSLMode.DISABLED) {
1192+
for (Endpoint endpoint : this.endpoints) {
1193+
if ("https".equalsIgnoreCase(endpoint.getURI().getScheme())) {
1194+
throw new ClientMisconfigurationException("SSL mode '" + SSLMode.DISABLED
1195+
+ "' cannot be used with a secure (https) endpoint. Use '" + SSLMode.TRUST
1196+
+ "' to trust all certificates or use plain HTTP.");
1197+
}
1198+
}
1199+
}
1200+
}
1201+
11431202
// Check timezone settings
11441203
String useTimeZoneValue = this.configuration.get(ClientConfigProperties.USE_TIMEZONE.getKey());
11451204
String serverTimeZoneValue = this.configuration.get(ClientConfigProperties.SERVER_TIMEZONE.getKey());

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.clickhouse.client.api;
22

33
import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader;
4+
import com.clickhouse.client.api.enums.SSLMode;
45
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
56
import com.clickhouse.data.ClickHouseDataType;
67
import com.clickhouse.data.ClickHouseFormat;
@@ -115,6 +116,8 @@ public enum ClientConfigProperties {
115116

116117
SSL_CERTIFICATE("sslcert", String.class),
117118

119+
SSL_MODE("ssl_mode", SSLMode.class, SSLMode.STRICT.name()),
120+
118121
RETRY_ON_FAILURE("retry", Integer.class, "3"),
119122

120123
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.clickhouse.client.api.enums;
2+
3+
/**
4+
* Defines how strictly the client verifies a server identity when a secure protocol is used.
5+
*
6+
* <p>The mode affects only connections that are already using a secure transport (for example,
7+
* an {@code https://} endpoint). It does <b>not</b> enable encryption for plain protocols - an
8+
* {@code http://} endpoint stays unencrypted whatever the mode is.</p>
9+
*
10+
* <p>Modes from the least to the most strict:</p>
11+
* <ul>
12+
* <li>{@link #DISABLED} - SSL is not used. Plain protocols only.</li>
13+
* <li>{@link #TRUST} - the hostname is not verified and any server certificate is accepted, which
14+
* is susceptible to MITM attacks - use that only for testing or in fully trusted environments. A
15+
* configured trust store or CA certificate has no effect in this mode and is ignored (a warning is
16+
* logged); a configured client certificate/key is still applied for mTLS.</li>
17+
* <li>{@link #VERIFY_CA} - the server certificate chain is validated against the trust material
18+
* (default JVM trust store, configured trust store, or a CA certificate), but the hostname is
19+
* not checked against the certificate.</li>
20+
* <li>{@link #STRICT} - full verification (default): certificate chain is validated and the
21+
* hostname must match the certificate.</li>
22+
* </ul>
23+
*/
24+
public enum SSLMode {
25+
26+
/**
27+
* SSL is not used. Connection is not encrypted. Doesn't work with HTTPS.
28+
* Reserved for TCP where protocol doesn't define encryption.
29+
*/
30+
DISABLED,
31+
32+
/**
33+
* The hostname is not verified and any server certificate is accepted. A configured trust store or
34+
* CA certificate has no effect in this mode and is ignored (a warning is logged). A configured
35+
* client certificate/key is still applied for mTLS.
36+
*/
37+
TRUST,
38+
39+
/**
40+
* Server certificate chain is validated, but the hostname is not verified.
41+
*/
42+
VERIFY_CA,
43+
44+
/**
45+
* Full verification: certificate chain is validated and the hostname must match
46+
* the certificate. Default mode for HTTPs.
47+
*/
48+
STRICT;
49+
50+
/**
51+
* Case-insensitive variant of {@link #valueOf(String)}.
52+
*
53+
* @param value mode name in any case
54+
* @return matching mode
55+
* @throws IllegalArgumentException when the value does not match any mode
56+
*/
57+
public static SSLMode fromValue(String value) {
58+
for (SSLMode mode : values()) {
59+
if (mode.name().equalsIgnoreCase(value)) {
60+
return mode;
61+
}
62+
}
63+
throw new IllegalArgumentException("Unknown SSL mode '" + value + "'");
64+
}
65+
}

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import com.clickhouse.client.api.DataTransferException;
1212
import com.clickhouse.client.api.ServerException;
1313
import com.clickhouse.client.api.enums.ProxyType;
14+
import com.clickhouse.client.api.enums.SSLMode;
1415
import com.clickhouse.client.api.http.ClickHouseHttpProto;
1516
import com.clickhouse.client.api.transport.Endpoint;
16-
import com.clickhouse.client.config.ClickHouseDefaultSslContextProvider;
1717
import com.clickhouse.data.ClickHouseFormat;
1818
import net.jpountz.lz4.LZ4Factory;
1919
import org.apache.commons.compress.compressors.CompressorStreamFactory;
@@ -85,7 +85,6 @@
8585
import java.net.URLEncoder;
8686
import java.net.UnknownHostException;
8787
import java.nio.charset.StandardCharsets;
88-
import java.security.NoSuchAlgorithmException;
8988
import java.util.Arrays;
9089
import java.util.Base64;
9190
import java.util.Collection;
@@ -131,7 +130,7 @@ public class HttpAPIClientHelper {
131130

132131
LZ4Factory lz4Factory;
133132

134-
private final ClickHouseDefaultSslContextProvider sslContextProvider = new ClickHouseDefaultSslContextProvider();
133+
private final SslContextProvider sslContextProvider = new SslContextProvider();
135134

136135
public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) {
137136
this.metricsRegistry = metricsRegistry;
@@ -159,34 +158,46 @@ public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegi
159158
* @return SSLContext
160159
*/
161160
public SSLContext createSSLContext(Map<String, Object> configuration) {
162-
SSLContext sslContext;
163-
try {
164-
sslContext = SSLContext.getDefault();
165-
} catch (NoSuchAlgorithmException e) {
166-
throw new ClientException("Failed to create default SSL context", e);
167-
}
161+
final SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration);
168162
final String trustStorePath = (String) configuration.get(ClientConfigProperties.SSL_TRUST_STORE.getKey());
169163
final String caCertificate = (String) configuration.get(ClientConfigProperties.CA_CERTIFICATE.getKey());
170164
final String sslCertificate = (String) configuration.get(ClientConfigProperties.SSL_CERTIFICATE.getKey());
171165
final String sslKey = (String) configuration.get(ClientConfigProperties.SSL_KEY.getKey());
172-
if (trustStorePath != null) {
173-
try {
174-
sslContext = sslContextProvider.getSslContextFromKeyStore(
175-
trustStorePath,
176-
(String) configuration.get(ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey()),
177-
(String) configuration.get(ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey())
178-
);
179-
} catch (SSLException e) {
180-
throw new ClientMisconfigurationException("Failed to create SSL context from a keystore", e);
166+
167+
SslContextProvider.Builder builder = sslContextProvider.builder();
168+
169+
// The client certificate/key (mTLS) are independent of how the server certificate is verified,
170+
// so they are applied whenever configured, regardless of the SSL mode.
171+
if (sslCertificate != null && !sslCertificate.isEmpty()) {
172+
builder.clientCertificate(sslCertificate, sslKey);
173+
}
174+
175+
if (sslMode == SSLMode.TRUST) {
176+
// TRUST accepts any server certificate and skips the hostname check (the latter is applied
177+
// where the connection socket factory is created). A configured trust store or CA
178+
// certificate has no effect in this mode and is ignored with a warning.
179+
if (trustStorePath != null || caCertificate != null) {
180+
LOG.warn("SSL mode '{}' trusts any server certificate; the configured {} is ignored.",
181+
SSLMode.TRUST, trustStorePath != null ? "trust store" : "CA certificate");
181182
}
182-
} else if (caCertificate != null || sslCertificate != null|| sslKey != null) {
183-
try {
184-
sslContext = sslContextProvider.getSslContextFromCerts(sslCertificate, sslKey, caCertificate);
185-
} catch (SSLException e) {
186-
throw new ClientMisconfigurationException("Failed to create SSL context from certificates", e);
183+
builder.trustAllCertificates();
184+
} else if (trustStorePath != null) {
185+
// VERIFY_CA / STRICT: validate against the trust store. A trust store and a CA certificate
186+
// cannot both take effect, so the CA certificate is ignored with a warning.
187+
if (caCertificate != null) {
188+
LOG.warn("Both a trust store and a CA certificate are configured; using the trust store and"
189+
+ " ignoring the CA certificate. Import the CA certificate into the trust store instead.");
187190
}
191+
builder.trustStore(trustStorePath,
192+
(String) configuration.get(ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey()),
193+
(String) configuration.get(ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey()));
194+
} else if (caCertificate != null) {
195+
// VERIFY_CA / STRICT: validate against the CA certificate.
196+
builder.rootCertificate(caCertificate);
188197
}
189-
return sslContext;
198+
// else VERIFY_CA / STRICT with no trust material: the JVM default trust store is used.
199+
200+
return builder.build();
190201
}
191202

192203
private static final long CONNECTION_INACTIVITY_CHECK = 5000L;
@@ -272,7 +283,11 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
272283
LayeredConnectionSocketFactory sslConnectionSocketFactory;
273284
if (sslContext != null) {
274285
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());
275-
if (socketSNI != null && !socketSNI.trim().isEmpty()) {
286+
SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration);
287+
// Trust and VerifyCa skip hostname verification. The same applies when a custom SNI is
288+
// set because the connection hostname will not match the certificate.
289+
boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA;
290+
if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) {
276291
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
277292
} else {
278293
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
@@ -881,6 +896,10 @@ public RuntimeException wrapException(String message, Exception cause, String qu
881896
return (RuntimeException) cause;
882897
}
883898

899+
if (cause instanceof SSLException) {
900+
return new ClickHouseException("SSL Problem", cause, queryId);
901+
}
902+
884903
if (cause instanceof ConnectionRequestTimeoutException ||
885904
cause instanceof NoHttpResponseException ||
886905
cause instanceof ConnectTimeoutException ||

0 commit comments

Comments
 (0)