Skip to content

Commit 613fe80

Browse files
fredjoonparkFred Park
andauthored
GH-1319: [JDBC] Add hostnameOverride connection property for TLS certificate verification and SNI (#1363)
## What's Changed Adds an optional `hostnameOverride` connection property to the Flight SQL JDBC driver. When TLS is enabled, the driver verifies the server certificate against this hostname and sends it for SNI and as the gRPC `:authority`, instead of using the host in the JDBC URL. This lets the driver connect with full certificate verification through SSH tunnels, port-forwarding, private endpoints, or IP addresses that don't match the server certificate. Today the only option in those cases is `disableCertificateVerification=true`. - New property `hostnameOverride` (string, default unset) in `ArrowFlightConnectionProperty`, passed from `ArrowFlightConnection` to `ArrowFlightSqlClientHandler.Builder`. - `Builder.build()` calls `NettyClientBuilder.overrideHostname(...)` when encryption is on. The property is ignored when `useEncryption=false`, and a blank value is treated as unset. - Extra endpoint locations in `getStreams()`: the override is applied only when the endpoint host and port match the original connection (host compared case-insensitively, IPv6 brackets ignored). Endpoints at any other location, or any endpoint when the original connection isn't encrypted, are verified against their own hostname, as they are today. This logic is in `Builder.getHostnameOverrideForEndpoint(URI)` so it can be unit-tested. - Documented in `docs/source/flight_sql_jdbc_driver.rst` next to `tlsRootCerts`. Example: ``` jdbc:arrow-flight-sql://192.0.2.10:443?useEncryption=true&tlsRootCerts=/path/ca.pem&hostnameOverride=flight.example.com ``` ## Tests - New `ConnectionTlsHostnameOverrideTest`. It uses the existing test certificate, which is issued for `localhost` and has no IP SAN. Its servers bind to `127.0.0.1` and the tests connect there, so they don't depend on how `localhost` resolves. Failure cases assert that the cause is a `CertificateException`, not just any `SQLException`: - without the property, the connection fails (`No subject alternative names present`); - with `hostnameOverride=localhost`, set in the URL or in `Properties`, it succeeds with full verification; - a non-matching override fails, which shows verification uses the override; - a blank `hostnameOverride=` behaves as unset; - the property is ignored for unencrypted connections; - a partitioned query whose endpoint is at the connection's host and port succeeds, which shows the override reaches endpoint clients; - a partitioned query whose endpoint is on the connection's IP but a different port fails, which shows the override is not applied there. - `ArrowFlightSqlClientHandlerBuilderTest`: the default is unset and the builder copy keeps the value. The endpoint rule is covered for same host and port, different port, no port, different case, IPv6 with and without brackets, different host, unencrypted connection, blank value, and no override set. - `ArrowFlightConnectionConfigImplTest`: the property is parsed. This change is additive and has no breaking changes. Closes #1319. --------- Co-authored-by: Fred Park <parkfrex@amazon.com>
1 parent 2ab1b02 commit 613fe80

7 files changed

Lines changed: 549 additions & 0 deletions

File tree

‎docs/source/flight_sql_jdbc_driver.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ case-sensitive. The supported parameters are:
118118
- Path to PEM-encoded root certificates for TLS - use this as
119119
an alternative to ``trustStore``
120120

121+
* - hostnameOverride
122+
- null
123+
- When TLS is enabled, the hostname to verify the server certificate
124+
against and to send for SNI and as the gRPC ``:authority`` header,
125+
instead of **HOSTNAME**. Use this when
126+
connecting through a tunnel, a private endpoint, or an IP address
127+
that does not match the server certificate. Endpoints returned by
128+
the server are only verified against this hostname if they are on
129+
the same host and port as the connection. Ignored when
130+
``useEncryption`` is false.
131+
121132
* - clientCertificate
122133
- null
123134
- Path to PEM-encoded client mTLS certificate when the Flight

‎flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ private static ArrowFlightSqlClientHandler createNewClientHandler(
119119
.withBufferAllocator(allocator)
120120
.withEncryption(config.useEncryption())
121121
.withDisableCertificateVerification(config.getDisableCertificateVerification())
122+
.withHostnameOverride(config.getHostnameOverride())
122123
.withToken(config.getToken())
123124
.withCallOptions(config.toCallOption())
124125
.withRetainCookies(config.retainCookies())

‎flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ public List<CloseableEndpointStreamPair> getStreams(final FlightInfo flightInfo)
188188
.withHost(endpointUri.getHost())
189189
.withPort(endpointUri.getPort())
190190
.withEncryption(endpointUri.getScheme().equals(LocationSchemes.GRPC_TLS))
191+
.withHostnameOverride(builder.getHostnameOverrideForEndpoint(endpointUri))
191192
.withClientCache(flightClientCache)
192193
.withConnectTimeout(builder.connectTimeout);
193194

@@ -677,6 +678,8 @@ public static final class Builder {
677678

678679
@VisibleForTesting boolean disableCertificateVerification;
679680

681+
@VisibleForTesting @Nullable String hostnameOverride;
682+
680683
@VisibleForTesting boolean useSystemTrustStore = true;
681684

682685
@VisibleForTesting String tlsRootCertificatesPath;
@@ -732,6 +735,7 @@ public Builder() {}
732735
this.token = original.token;
733736
this.useEncryption = original.useEncryption;
734737
this.disableCertificateVerification = original.disableCertificateVerification;
738+
this.hostnameOverride = original.hostnameOverride;
735739
this.useSystemTrustStore = original.useSystemTrustStore;
736740
this.tlsRootCertificatesPath = original.tlsRootCertificatesPath;
737741
this.clientCertificatePath = original.clientCertificatePath;
@@ -840,6 +844,54 @@ public Builder withDisableCertificateVerification(
840844
return this;
841845
}
842846

847+
/**
848+
* Sets the hostname to use for TLS certificate verification and SNI instead of the host this
849+
* handler connects to. It is also sent as the gRPC authority ({@code :authority} header).
850+
* Ignored when encryption is disabled.
851+
*
852+
* @param hostnameOverride the hostname to verify the server certificate against, or null or
853+
* blank to use the host.
854+
* @return this instance.
855+
*/
856+
public Builder withHostnameOverride(@Nullable final String hostnameOverride) {
857+
final String trimmed = hostnameOverride == null ? null : hostnameOverride.trim();
858+
this.hostnameOverride = trimmed == null || trimmed.isEmpty() ? null : trimmed;
859+
return this;
860+
}
861+
862+
/**
863+
* Gets the hostname override to use when connecting to an endpoint location. The override is
864+
* only valid for the host and port it was configured for, so endpoints at any other location
865+
* are verified against their own hostname. Like the override itself, it is ignored when the
866+
* original connection does not use encryption.
867+
*
868+
* @param endpointUri the endpoint location.
869+
* @return the hostname override if the endpoint is on the original host and port, otherwise
870+
* null.
871+
*/
872+
@VisibleForTesting
873+
@Nullable String getHostnameOverrideForEndpoint(final URI endpointUri) {
874+
final String endpointHost = stripBrackets(endpointUri.getHost());
875+
if (!useEncryption
876+
|| hostnameOverride == null
877+
|| endpointHost == null
878+
|| endpointUri.getPort() != port
879+
|| !endpointHost.equalsIgnoreCase(stripBrackets(host))) {
880+
return null;
881+
}
882+
return hostnameOverride;
883+
}
884+
885+
/**
886+
* Strips the brackets from an IPv6 literal. {@link URI#getHost()} keeps them, but a host set
887+
* through properties may not have them.
888+
*/
889+
private static @Nullable String stripBrackets(@Nullable final String host) {
890+
return host != null && host.startsWith("[") && host.endsWith("]")
891+
? host.substring(1, host.length() - 1)
892+
: host;
893+
}
894+
843895
/**
844896
* Sets whether to use the certificates from the operating system.
845897
*
@@ -1070,6 +1122,10 @@ public ArrowFlightSqlClientHandler build() throws SQLException {
10701122
clientBuilder.location(location);
10711123

10721124
if (useEncryption) {
1125+
if (hostnameOverride != null) {
1126+
clientBuilder.overrideHostname(hostnameOverride);
1127+
}
1128+
10731129
if (disableCertificateVerification) {
10741130
clientBuilder.verifyServer(false);
10751131
} else {

‎flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ public String getTlsRootCertificatesPath() {
112112
return ArrowFlightConnectionProperty.TLS_ROOT_CERTS.getString(properties);
113113
}
114114

115+
/**
116+
* Gets the hostname to use for TLS certificate verification and SNI instead of the host.
117+
*
118+
* @return the hostname override, or null if not set.
119+
*/
120+
public String getHostnameOverride() {
121+
return ArrowFlightConnectionProperty.HOSTNAME_OVERRIDE.getString(properties);
122+
}
123+
115124
public String getClientCertificatePath() {
116125
return ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.getString(properties);
117126
}
@@ -258,6 +267,7 @@ public enum ArrowFlightConnectionProperty implements ConnectionProperty {
258267
TRUST_STORE_PASSWORD("trustStorePassword", null, Type.STRING, false),
259268
USE_SYSTEM_TRUST_STORE("useSystemTrustStore", true, Type.BOOLEAN, false),
260269
TLS_ROOT_CERTS("tlsRootCerts", null, Type.STRING, false),
270+
HOSTNAME_OVERRIDE("hostnameOverride", null, Type.STRING, false),
261271
CLIENT_CERTIFICATE("clientCertificate", null, Type.STRING, false),
262272
CLIENT_KEY("clientKey", null, Type.STRING, false),
263273
THREAD_POOL_SIZE("threadPoolSize", 1, Type.NUMBER, false),

0 commit comments

Comments
 (0)