From e4515cdc929888d97dce18007101cb7b9dfcc08e Mon Sep 17 00:00:00 2001 From: jon-wei Date: Wed, 26 Sep 2018 16:03:01 -0700 Subject: [PATCH] Allow custom TLS cert checks --- .../simple-client-sslcontext.md | 1 + docs/content/operations/tls-support.md | 13 +++ .../apache/druid/https/SSLClientConfig.java | 9 ++ .../druid/https/SSLContextProvider.java | 10 +- integration-tests/docker/Dockerfile | 1 + .../docker/router-custom-check-tls.conf | 57 +++++++++ integration-tests/run_cluster.sh | 8 +- .../testing/ConfigFileConfigProvider.java | 29 +++++ .../druid/testing/DockerConfigProvider.java | 12 ++ .../testing/IntegrationTestingConfig.java | 4 + .../testing/guice/DruidTestModuleFactory.java | 1 - .../guice/ITTLSCertificateCheckerModule.java | 53 +++++++++ .../utils/ITTLSCertificateChecker.java | 57 +++++++++ ...rg.apache.druid.initialization.DruidModule | 1 + .../druid/tests/security/ITTLSTest.java | 78 +++++++++++-- integration-tests/stop_cluster.sh | 2 +- .../druid/initialization/Initialization.java | 2 + .../jetty/ChatHandlerServerModule.java | 4 +- .../jetty/JettyServerModule.java | 51 +++++++- .../security/CustomCheckX509TrustManager.java | 102 ++++++++++++++++ .../DefaultTLSCertificateChecker.java | 48 ++++++++ .../DefaultTLSCertificateCheckerModule.java | 41 +++++++ .../security/TLSCertificateChecker.java | 79 +++++++++++++ .../security/TLSCertificateCheckerModule.java | 109 ++++++++++++++++++ .../druid/server/security/TLSUtils.java | 41 ++++++- 25 files changed, 789 insertions(+), 24 deletions(-) create mode 100644 integration-tests/docker/router-custom-check-tls.conf create mode 100644 integration-tests/src/main/java/org/apache/druid/testing/guice/ITTLSCertificateCheckerModule.java create mode 100644 integration-tests/src/main/java/org/apache/druid/testing/utils/ITTLSCertificateChecker.java create mode 100644 integration-tests/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule create mode 100644 server/src/main/java/org/apache/druid/server/security/CustomCheckX509TrustManager.java create mode 100644 server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateChecker.java create mode 100644 server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateCheckerModule.java create mode 100644 server/src/main/java/org/apache/druid/server/security/TLSCertificateChecker.java create mode 100644 server/src/main/java/org/apache/druid/server/security/TLSCertificateCheckerModule.java diff --git a/docs/content/development/extensions-core/simple-client-sslcontext.md b/docs/content/development/extensions-core/simple-client-sslcontext.md index 31ae3d6eae7b..8ff8d0cfd794 100644 --- a/docs/content/development/extensions-core/simple-client-sslcontext.md +++ b/docs/content/development/extensions-core/simple-client-sslcontext.md @@ -28,6 +28,7 @@ The following table contains optional parameters for supporting client certifica |`druid.client.https.keyStorePassword`|The [Password Provider](../operations/password-provider.html) or String password for the Key Store.|none|no| |`druid.client.https.keyManagerFactoryAlgorithm`|Algorithm to use for creating KeyManager, more details [here](https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#KeyManager).|`javax.net.ssl.KeyManagerFactory.getDefaultAlgorithm()`|no| |`druid.client.https.keyManagerPassword`|The [Password Provider](../operations/password-provider.html) or String password for the Key Manager.|none|no| +|`druid.client.https.validateHostnames`|Validate the hostname of the server. This should not be disabled unless you are using [custom TLS certificate checks](../../operations/tls-support.html#custom-tls-certificate-checks) and know that standard hostname validation is not needed.|true|no| This [document](http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html) lists all the possible values for the above mentioned configs among others provided by Java implementation. \ No newline at end of file diff --git a/docs/content/operations/tls-support.md b/docs/content/operations/tls-support.md index 63da285a3157..788bf215c749 100644 --- a/docs/content/operations/tls-support.md +++ b/docs/content/operations/tls-support.md @@ -71,3 +71,16 @@ to create your own extension. When Druid Coordinator/Overlord have both HTTP and HTTPS enabled and Client sends request to non-leader node, then Client is always redirected to the HTTPS endpoint on leader node. So, Clients should be first upgraded to be able to handle redirect to HTTPS. Then Druid Overlord/Coordinator should be upgraded and configured to run both HTTP and HTTPS ports. Then Client configuration should be changed to refer to Druid Coordinator/Overlord via the HTTPS endpoint and then HTTP port on Druid Coordinator/Overlord should be disabled. +# Custom TLS certificate checks + +Druid supports custom certificate check extensions. Please refer to the `org.apache.druid.server.security.TLSCertificateChecker` interface for details on the methods to be implemented. + +To use a custom TLS certificate checker, specify the following property: + +|Property|Description|Default|Required| +|--------|-----------|-------|--------| +|`druid.tls.certificateChecker`|Type name of custom TLS certificate checker, provided by extensions.|"default"|no| + +The default checker delegates to the base trust manager and performs no additional actions or checks. + +If using a non-default certificate checker, please refer to the extension documentation for additional configuration properties needed. diff --git a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java index b7061cb84f3b..0bcf0b353003 100644 --- a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java +++ b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java @@ -57,6 +57,9 @@ public class SSLClientConfig @JsonProperty private String keyManagerFactoryAlgorithm; + @JsonProperty + private Boolean validateHostnames; + public String getProtocol() { return protocol; @@ -112,6 +115,11 @@ public String getKeyManagerFactoryAlgorithm() return keyManagerFactoryAlgorithm; } + public Boolean getValidateHostnames() + { + return validateHostnames; + } + @Override public String toString() { @@ -124,6 +132,7 @@ public String toString() ", keyStoreType='" + keyStoreType + '\'' + ", certAlias='" + certAlias + '\'' + ", keyManagerFactoryAlgorithm='" + keyManagerFactoryAlgorithm + '\'' + + ", validateHostnames='" + validateHostnames + '\'' + '}'; } } diff --git a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java index 826791deb642..e5e6114337ed 100644 --- a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java +++ b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java @@ -22,6 +22,7 @@ import com.google.inject.Inject; import com.google.inject.Provider; import org.apache.druid.java.util.emitter.EmittingLogger; +import org.apache.druid.server.security.TLSCertificateChecker; import org.apache.druid.server.security.TLSUtils; import javax.net.ssl.SSLContext; @@ -31,11 +32,16 @@ public class SSLContextProvider implements Provider private static final EmittingLogger log = new EmittingLogger(SSLContextProvider.class); private SSLClientConfig config; + private TLSCertificateChecker certificateChecker; @Inject - public SSLContextProvider(SSLClientConfig config) + public SSLContextProvider( + SSLClientConfig config, + TLSCertificateChecker certificateChecker + ) { this.config = config; + this.certificateChecker = certificateChecker; } @Override @@ -55,6 +61,8 @@ public SSLContext get() .setCertAlias(config.getCertAlias()) .setKeyStorePasswordProvider(config.getKeyStorePasswordProvider()) .setKeyManagerFactoryPasswordProvider(config.getKeyManagerPasswordProvider()) + .setValidateHostnames(config.getValidateHostnames()) + .setCertificateChecker(certificateChecker) .build(); } } diff --git a/integration-tests/docker/Dockerfile b/integration-tests/docker/Dockerfile index 35deb9af44cc..2b46de7f06fd 100644 --- a/integration-tests/docker/Dockerfile +++ b/integration-tests/docker/Dockerfile @@ -67,6 +67,7 @@ ADD client_tls client_tls # - 8083, 8283: HTTP, HTTPS (historical) # - 8090, 8290: HTTP, HTTPS (overlord) # - 8091, 8291: HTTP, HTTPS (middlemanager) +# - 8888-8891, 9088-9091: HTTP, HTTPS (routers) # - 3306: MySQL # - 2181 2888 3888: ZooKeeper # - 8100 8101 8102 8103 8104 8105 : peon ports diff --git a/integration-tests/docker/router-custom-check-tls.conf b/integration-tests/docker/router-custom-check-tls.conf new file mode 100644 index 000000000000..4ecaaebd75c4 --- /dev/null +++ b/integration-tests/docker/router-custom-check-tls.conf @@ -0,0 +1,57 @@ +[program:druid-router-custom-check-tls] +command=java + -server + -Xmx128m + -XX:+UseConcMarkSweepGC + -XX:+PrintGCDetails + -XX:+PrintGCTimeStamps + -Duser.timezone=UTC + -Dfile.encoding=UTF-8 + -Ddruid.host=%(ENV_HOST_IP)s + -Ddruid.plaintextPort=8891 + -Ddruid.tlsPort=9091 + -Ddruid.zk.service.host=druid-zookeeper-kafka + -Ddruid.server.http.numThreads=100 + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.router.managementProxy.enabled=true + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -Ddruid.auth.authenticator.basic.initialAdminPassword=priest + -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock + -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router-custom-check-tls + -Ddruid.escalator.type=basic + -Ddruid.escalator.internalClientUsername=druid_system + -Ddruid.escalator.internalClientPassword=warlock + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic + -Ddruid.sql.enable=true + -Ddruid.sql.avatica.enable=true + -Ddruid.enableTlsPort=true + -Ddruid.server.https.keyStorePath=/tls/server.jks + -Ddruid.server.https.keyStoreType=jks + -Ddruid.server.https.certAlias=druid + -Ddruid.server.https.keyManagerPassword=druid123 + -Ddruid.server.https.keyStorePassword=druid123 + -Ddruid.server.https.requireClientCertificate=true + -Ddruid.server.https.trustStorePath=/tls/truststore.jks + -Ddruid.server.https.trustStorePassword=druid123 + -Ddruid.server.https.trustStoreAlgorithm=PKIX + -Ddruid.server.https.validateHostnames=true + -Ddruid.client.https.trustStoreAlgorithm=PKIX + -Ddruid.client.https.protocol=TLSv1.2 + -Ddruid.client.https.trustStorePath=/tls/truststore.jks + -Ddruid.client.https.trustStorePassword=druid123 + -Ddruid.client.https.keyStorePath=/tls/server.jks + -Ddruid.client.https.certAlias=druid + -Ddruid.client.https.keyManagerPassword=druid123 + -Ddruid.client.https.keyStorePassword=druid123 + -Ddruid.client.https.validateHostnames=false + -Ddruid.tls.certificateChecker=integration-test + -cp /shared/docker/lib/* + org.apache.druid.cli.Main server router +redirect_stderr=true +priority=100 +autorestart=false +stdout_logfile=/shared/logs/router-custom-check-tls.log \ No newline at end of file diff --git a/integration-tests/run_cluster.sh b/integration-tests/run_cluster.sh index 18a98d418058..c08867ab59cb 100755 --- a/integration-tests/run_cluster.sh +++ b/integration-tests/run_cluster.sh @@ -15,7 +15,7 @@ # limitations under the License. # cleanup -for node in druid-historical druid-coordinator druid-overlord druid-router druid-router-permissive-tls druid-router-no-client-auth-tls druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage; +for node in druid-historical druid-coordinator druid-overlord druid-router druid-router-permissive-tls druid-router-no-client-auth-tls druid-router-custom-check-tls druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage; do docker stop $node docker rm $node @@ -50,6 +50,9 @@ mvn -B dependency:copy-dependencies -DoutputDirectory=$SHARED_DIR/docker/lib # install logging config cp src/main/resources/log4j2.xml $SHARED_DIR/docker/lib/log4j2.xml +# copy the integration test jar, it provides test-only extension implementations +cp target/druid-integration-tests*.jar $SHARED_DIR/docker/lib + docker network create --subnet=172.172.172.0/24 druid-it-net # Build Druid Cluster Image @@ -84,3 +87,6 @@ docker run -d --privileged --net druid-it-net --ip 172.172.172.10 --name druid-r # Start Router with TLS but no client auth docker run -d --privileged --net druid-it-net --ip 172.172.172.11 --name druid-router-no-client-auth-tls -p 8890:8890 -p 9090:9090 -v $SHARED_DIR:/shared -v $DOCKERDIR/router-no-client-auth-tls.conf:$SUPERVISORDIR/router-no-client-auth-tls.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-coordinator:druid-coordinator --link druid-broker:druid-broker druid/cluster + +# Start Router with custom TLS cert checkers +docker run -d --privileged --net druid-it-net --ip 172.172.172.12 --hostname druid-router-custom-check-tls --name druid-router-custom-check-tls -p 8891:8891 -p 9091:9091 -v $SHARED_DIR:/shared -v $DOCKERDIR/router-custom-check-tls.conf:$SUPERVISORDIR/router-custom-check-tls.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-coordinator:druid-coordinator --link druid-broker:druid-broker druid/cluster diff --git a/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java b/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java index 27805278d47d..990b91805768 100644 --- a/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java +++ b/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java @@ -40,6 +40,7 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide private String indexerUrl; private String permissiveRouterUrl; private String noClientAuthRouterUrl; + private String customCertCheckRouterUrl; private String routerTLSUrl; private String brokerTLSUrl; private String historicalTLSUrl; @@ -47,6 +48,7 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide private String indexerTLSUrl; private String permissiveRouterTLSUrl; private String noClientAuthRouterTLSUrl; + private String customCertCheckRouterTLSUrl; private String middleManagerHost; private String zookeeperHosts; // comma-separated list of host:port private String kafkaHost; @@ -114,6 +116,21 @@ private void loadProperties(String configFile) noClientAuthRouterTLSUrl = StringUtils.format("https://%s:%s", noClientAuthRouterHost, props.get("router_no_client_auth_tls_port")); } } + customCertCheckRouterUrl = props.get("router_no_client_auth_url"); + if (customCertCheckRouterUrl == null) { + String customCertCheckRouterHost = props.get("router_no_client_auth_host"); + if (null != customCertCheckRouterHost) { + customCertCheckRouterUrl = StringUtils.format("http://%s:%s", customCertCheckRouterHost, props.get("router_no_client_auth_port")); + } + } + customCertCheckRouterTLSUrl = props.get("router_no_client_auth_tls_url"); + if (customCertCheckRouterTLSUrl == null) { + String customCertCheckRouterHost = props.get("router_no_client_auth_host"); + if (null != customCertCheckRouterHost) { + customCertCheckRouterTLSUrl = StringUtils.format("https://%s:%s", customCertCheckRouterHost, props.get("router_no_client_auth_tls_port")); + } + } + brokerUrl = props.get("broker_url"); if (brokerUrl == null) { brokerUrl = StringUtils.format("http://%s:%s", props.get("broker_host"), props.get("broker_port")); @@ -248,6 +265,18 @@ public String getNoClientAuthRouterTLSUrl() return noClientAuthRouterTLSUrl; } + @Override + public String getCustomCertCheckRouterUrl() + { + return customCertCheckRouterUrl; + } + + @Override + public String getCustomCertCheckRouterTLSUrl() + { + return customCertCheckRouterTLSUrl; + } + @Override public String getBrokerUrl() { diff --git a/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java b/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java index bb71bc3bf4cb..04d512a15cdf 100644 --- a/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java +++ b/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java @@ -102,6 +102,18 @@ public String getNoClientAuthRouterTLSUrl() return "https://" + dockerIp + ":9090"; } + @Override + public String getCustomCertCheckRouterUrl() + { + return "http://" + dockerIp + ":8891"; + } + + @Override + public String getCustomCertCheckRouterTLSUrl() + { + return "https://" + dockerIp + ":9091"; + } + @Override public String getBrokerUrl() { diff --git a/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java b/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java index 2b72e58c9d68..ec321a035f56 100644 --- a/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java +++ b/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java @@ -45,6 +45,10 @@ public interface IntegrationTestingConfig String getNoClientAuthRouterTLSUrl(); + String getCustomCertCheckRouterUrl(); + + String getCustomCertCheckRouterTLSUrl(); + String getBrokerUrl(); String getBrokerTLSUrl(); diff --git a/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModuleFactory.java b/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModuleFactory.java index 471ba4791000..90a220835a9d 100644 --- a/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModuleFactory.java +++ b/integration-tests/src/main/java/org/apache/druid/testing/guice/DruidTestModuleFactory.java @@ -59,5 +59,4 @@ public Module createModule(ITestContext context, Class testClass) context.addInjector(Collections.singletonList(module), injector); return module; } - } diff --git a/integration-tests/src/main/java/org/apache/druid/testing/guice/ITTLSCertificateCheckerModule.java b/integration-tests/src/main/java/org/apache/druid/testing/guice/ITTLSCertificateCheckerModule.java new file mode 100644 index 000000000000..f5c24a16da4f --- /dev/null +++ b/integration-tests/src/main/java/org/apache/druid/testing/guice/ITTLSCertificateCheckerModule.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.testing.guice; + +import com.fasterxml.jackson.databind.Module; +import com.google.inject.Binder; + +import com.google.inject.name.Names; +import org.apache.druid.initialization.DruidModule; +import org.apache.druid.server.security.TLSCertificateChecker; +import org.apache.druid.testing.utils.ITTLSCertificateChecker; + +import java.util.Collections; +import java.util.List; + +public class ITTLSCertificateCheckerModule implements DruidModule +{ + private final ITTLSCertificateChecker INSTANCE = new ITTLSCertificateChecker(); + + public static final String IT_CHECKER_TYPE = "integration-test"; + + @Override + public void configure(Binder binder) + { + binder.bind(TLSCertificateChecker.class) + .annotatedWith(Names.named(IT_CHECKER_TYPE)) + .toInstance(INSTANCE); + } + + @Override + public List getJacksonModules() + { + return Collections.EMPTY_LIST; + } +} + diff --git a/integration-tests/src/main/java/org/apache/druid/testing/utils/ITTLSCertificateChecker.java b/integration-tests/src/main/java/org/apache/druid/testing/utils/ITTLSCertificateChecker.java new file mode 100644 index 000000000000..9118ee69a55c --- /dev/null +++ b/integration-tests/src/main/java/org/apache/druid/testing/utils/ITTLSCertificateChecker.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.testing.utils; + +import org.apache.druid.java.util.common.logger.Logger; +import org.apache.druid.server.security.TLSCertificateChecker; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class ITTLSCertificateChecker implements TLSCertificateChecker +{ + private static final Logger log = new Logger(ITTLSCertificateChecker.class); + + @Override + public void checkClient( + X509Certificate[] chain, String authType, SSLEngine engine, X509ExtendedTrustManager baseTrustManager + ) throws CertificateException + { + // only the integration test client with "thisisprobablynottherighthostname" cert is allowed to talk to me + if (!chain[0].toString().contains("thisisprobablynottherighthostname") || !engine.getPeerHost().contains("172.172.172.1")) { + throw new CertificateException("Custom check rejected request from client."); + } + } + + @Override + public void checkServer( + X509Certificate[] chain, String authType, SSLEngine engine, X509ExtendedTrustManager baseTrustManager + ) throws CertificateException + { + baseTrustManager.checkServerTrusted(chain, authType, engine); + + // fail intentionally when trying to talk to the broker + if (chain[0].toString().contains("172.172.172.8")) { + throw new CertificateException("Custom check intentionally terminated request to broker."); + } + } +} diff --git a/integration-tests/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule b/integration-tests/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule new file mode 100644 index 000000000000..d023f3bce61a --- /dev/null +++ b/integration-tests/src/main/resources/META-INF/services/org.apache.druid.initialization.DruidModule @@ -0,0 +1 @@ +org.apache.druid.testing.guice.ITTLSCertificateCheckerModule diff --git a/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java b/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java index 82c5f9ac16c7..1b67c6407b8c 100644 --- a/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java +++ b/integration-tests/src/test/java/org/apache/druid/tests/security/ITTLSTest.java @@ -38,9 +38,11 @@ import org.apache.druid.java.util.http.client.auth.BasicCredentials; import org.apache.druid.java.util.http.client.response.StatusResponseHandler; import org.apache.druid.java.util.http.client.response.StatusResponseHolder; +import org.apache.druid.server.security.TLSCertificateChecker; import org.apache.druid.server.security.TLSUtils; import org.apache.druid.testing.IntegrationTestingConfig; import org.apache.druid.testing.guice.DruidTestModuleFactory; +import org.apache.druid.testing.utils.ITTLSCertificateChecker; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.joda.time.Duration; @@ -81,6 +83,9 @@ public class ITTLSTest @Client DruidHttpClientConfig httpClientConfig; + @Inject + TLSCertificateChecker certificateChecker; + StatusResponseHandler responseHandler = new StatusResponseHandler(StandardCharsets.UTF_8); @Test @@ -234,6 +239,33 @@ public void checkAccessWithNotCASignedCert() makeRequest(notCAClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null); } + @Test + public void checkAccessWithCustomCertificateChecks() + { + LOG.info("---------Testing TLS resource access with custom certificate checks---------"); + HttpClient wrongHostnameClient = makeCustomHttpClient( + "client_tls/invalid_hostname_client.jks", + "invalid_hostname_client", + new ITTLSCertificateChecker() + ); + + checkFailedAccessWrongHostname(httpClient, HttpMethod.GET, config.getCustomCertCheckRouterTLSUrl()); + + makeRequest(wrongHostnameClient, HttpMethod.GET, config.getCustomCertCheckRouterTLSUrl() + "/status", null); + + checkFailedAccess( + wrongHostnameClient, + HttpMethod.POST, + config.getCustomCertCheckRouterTLSUrl() + "/druid/v2", + "Custom cert check", + ISE.class, + "Error while making request to url[https://127.0.0.1:9091/druid/v2] status[400 Bad Request] content[{\"error\":\"No content to map due to end-of-input", + true + ); + + makeRequest(wrongHostnameClient, HttpMethod.GET, config.getCustomCertCheckRouterTLSUrl() + "/druid/coordinator/v1/leader", null); + } + private void checkFailedAccessNoCert(HttpClient httpClient, HttpMethod method, String url) { checkFailedAccess( @@ -242,7 +274,8 @@ private void checkFailedAccessNoCert(HttpClient httpClient, HttpMethod method, S url + "/status", "Certless", SSLException.class, - "Received fatal alert: bad_certificate" + "Received fatal alert: bad_certificate", + false ); } @@ -254,7 +287,8 @@ private void checkFailedAccessWrongHostname(HttpClient httpClient, HttpMethod me url + "/status", "Wrong hostname", SSLException.class, - "Received fatal alert: certificate_unknown" + "Received fatal alert: certificate_unknown", + false ); } @@ -266,7 +300,8 @@ private void checkFailedAccessWrongRoot(HttpClient httpClient, HttpMethod method url + "/status", "Wrong root cert", SSLException.class, - "Received fatal alert: certificate_unknown" + "Received fatal alert: certificate_unknown", + false ); } @@ -278,7 +313,8 @@ private void checkFailedAccessRevoked(HttpClient httpClient, HttpMethod method, url + "/status", "Revoked cert", SSLException.class, - "Received fatal alert: certificate_unknown" + "Received fatal alert: certificate_unknown", + false ); } @@ -290,7 +326,8 @@ private void checkFailedAccessExpired(HttpClient httpClient, HttpMethod method, url + "/status", "Expired cert", SSLException.class, - "Received fatal alert: certificate_unknown" + "Received fatal alert: certificate_unknown", + false ); } @@ -302,7 +339,8 @@ private void checkFailedAccessNotCA(HttpClient httpClient, HttpMethod method, St url + "/status", "Cert signed by non-CA", SSLException.class, - "Received fatal alert: certificate_unknown" + "Received fatal alert: certificate_unknown", + false ); } @@ -322,6 +360,15 @@ private HttpClientConfig.Builder getHttpClientConfigBuilder(SSLContext sslContex } private HttpClient makeCustomHttpClient(String keystorePath, String certAlias) + { + return makeCustomHttpClient(keystorePath, certAlias, certificateChecker); + } + + private HttpClient makeCustomHttpClient( + String keystorePath, + String certAlias, + TLSCertificateChecker certificateChecker + ) { SSLContext intermediateClientSSLContext = new TLSUtils.ClientSSLContextBuilder() .setProtocol(sslClientConfig.getProtocol()) @@ -335,6 +382,7 @@ private HttpClient makeCustomHttpClient(String keystorePath, String certAlias) .setCertAlias(certAlias) .setKeyStorePasswordProvider(sslClientConfig.getKeyStorePasswordProvider()) .setKeyManagerFactoryPasswordProvider(sslClientConfig.getKeyManagerPasswordProvider()) + .setCertificateChecker(certificateChecker) .build(); final HttpClientConfig.Builder builder = getHttpClientConfigBuilder(intermediateClientSSLContext); @@ -361,6 +409,7 @@ private HttpClient makeCertlessClient() .setTrustStorePath(sslClientConfig.getTrustStorePath()) .setTrustStoreAlgorithm(sslClientConfig.getTrustStoreAlgorithm()) .setTrustStorePasswordProvider(sslClientConfig.getTrustStorePasswordProvider()) + .setCertificateChecker(certificateChecker) .build(); final HttpClientConfig.Builder builder = getHttpClientConfigBuilder(certlessClientSSLContext); @@ -385,7 +434,8 @@ private void checkFailedAccess( String url, String clientDesc, Class expectedException, - String expectedExceptionMsg + String expectedExceptionMsg, + boolean useContainsMsgCheck ) { int retries = 0; @@ -411,13 +461,17 @@ private void checkFailedAccess( Assert.assertTrue( expectedException.isInstance(rootCause), - StringUtils.format("Expected %s but found %s instead.", expectedException, rootCause) + StringUtils.format("Expected %s but found %s instead.", expectedException, Throwables.getStackTraceAsString(rootCause)) ); - Assert.assertEquals( - rootCause.getMessage(), - expectedExceptionMsg - ); + if (useContainsMsgCheck) { + Assert.assertTrue(rootCause.getMessage().contains(expectedExceptionMsg)); + } else { + Assert.assertEquals( + rootCause.getMessage(), + expectedExceptionMsg + ); + } LOG.info("%s client [%s] request failed as expected when accessing [%s]", clientDesc, method, url); return; diff --git a/integration-tests/stop_cluster.sh b/integration-tests/stop_cluster.sh index fe4ad202fe57..d49dbed93570 100755 --- a/integration-tests/stop_cluster.sh +++ b/integration-tests/stop_cluster.sh @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -for node in druid-historical druid-coordinator druid-overlord druid-router druid-router-permissive-tls druid-router-no-client-auth-tls druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage; +for node in druid-historical druid-coordinator druid-overlord druid-router druid-router-permissive-tls druid-router-no-client-auth-tls druid-router-custom-check-tls druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage; do docker stop $node diff --git a/server/src/main/java/org/apache/druid/initialization/Initialization.java b/server/src/main/java/org/apache/druid/initialization/Initialization.java index 9d8c1019e9bd..a72349b8bea3 100644 --- a/server/src/main/java/org/apache/druid/initialization/Initialization.java +++ b/server/src/main/java/org/apache/druid/initialization/Initialization.java @@ -70,6 +70,7 @@ import org.apache.druid.server.initialization.AuthorizerMapperModule; import org.apache.druid.server.initialization.jetty.JettyServerModule; import org.apache.druid.server.metrics.MetricsModule; +import org.apache.druid.server.security.TLSCertificateCheckerModule; import org.eclipse.aether.artifact.DefaultArtifact; import java.io.File; @@ -369,6 +370,7 @@ public static Injector makeInjectorWithModules(final Injector baseInjector, Iter new Log4jShutterDownerModule(), new DruidAuthModule(), new LifecycleModule(), + TLSCertificateCheckerModule.class, EmitterModule.class, HttpClientModule.global(), HttpClientModule.escalatedGlobal(), diff --git a/server/src/main/java/org/apache/druid/server/initialization/jetty/ChatHandlerServerModule.java b/server/src/main/java/org/apache/druid/server/initialization/jetty/ChatHandlerServerModule.java index 2a1a0f5662f2..6eafec811402 100644 --- a/server/src/main/java/org/apache/druid/server/initialization/jetty/ChatHandlerServerModule.java +++ b/server/src/main/java/org/apache/druid/server/initialization/jetty/ChatHandlerServerModule.java @@ -36,6 +36,7 @@ import org.apache.druid.server.initialization.ServerConfig; import org.apache.druid.server.initialization.TLSServerConfig; import org.apache.druid.server.metrics.DataSourceTaskIdHolder; +import org.apache.druid.server.security.TLSCertificateChecker; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -101,7 +102,8 @@ public Server getServer( node, config, TLSServerConfig, - injector.getExistingBinding(Key.get(SslContextFactory.class)) + injector.getExistingBinding(Key.get(SslContextFactory.class)), + injector.getInstance(TLSCertificateChecker.class) ); } } diff --git a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java index bb0cb5147ea6..ff392d238124 100644 --- a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java +++ b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java @@ -60,6 +60,8 @@ import org.apache.druid.server.metrics.DataSourceTaskIdHolder; import org.apache.druid.server.metrics.MetricsModule; import org.apache.druid.server.metrics.MonitorsConfig; +import org.apache.druid.server.security.CustomCheckX509TrustManager; +import org.apache.druid.server.security.TLSCertificateChecker; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; @@ -75,10 +77,14 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; import java.security.KeyStore; +import java.security.cert.CRL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -159,7 +165,8 @@ public Server getServer( node, config, TLSServerConfig, - injector.getExistingBinding(Key.get(SslContextFactory.class)) + injector.getExistingBinding(Key.get(SslContextFactory.class)), + injector.getInstance(TLSCertificateChecker.class) ); } @@ -187,7 +194,8 @@ static Server makeAndInitializeServer( DruidNode node, ServerConfig config, TLSServerConfig tlsServerConfig, - Binding sslContextFactoryBinding + Binding sslContextFactoryBinding, + TLSCertificateChecker certificateChecker ) { // adjusting to make config.getNumThreads() mean, "number of threads @@ -235,7 +243,7 @@ static Server makeAndInitializeServer( log.info("Creating https connector with port [%d]", node.getTlsPort()); if (sslContextFactoryBinding == null) { // Never trust all certificates by default - sslContextFactory = new SslContextFactory(false); + sslContextFactory = new IdentityCheckOverrideSslContextFactory(tlsServerConfig, certificateChecker); sslContextFactory.setKeyStorePath(tlsServerConfig.getKeyStorePath()); sslContextFactory.setKeyStoreType(tlsServerConfig.getKeyStoreType()); @@ -471,4 +479,41 @@ public boolean doMonitor(ServiceEmitter emitter) return true; } } + + private static class IdentityCheckOverrideSslContextFactory extends SslContextFactory + { + private final TLSServerConfig tlsServerConfig; + private final TLSCertificateChecker certificateChecker; + + public IdentityCheckOverrideSslContextFactory( + TLSServerConfig tlsServerConfig, + TLSCertificateChecker certificateChecker + ) + { + super(false); + this.tlsServerConfig = tlsServerConfig; + this.certificateChecker = certificateChecker; + } + + @Override + protected TrustManager[] getTrustManagers( + KeyStore trustStore, Collection crls + ) throws Exception + { + TrustManager[] trustManagers = super.getTrustManagers(trustStore, crls); + TrustManager[] newTrustManagers = new TrustManager[trustManagers.length]; + + for (int i = 0; i < trustManagers.length; i++) { + if (trustManagers[i] instanceof X509ExtendedTrustManager) { + newTrustManagers[i] = new CustomCheckX509TrustManager( + (X509ExtendedTrustManager) trustManagers[i], + certificateChecker, + tlsServerConfig.isValidateHostnames() + ); + } + } + + return newTrustManagers; + } + } } diff --git a/server/src/main/java/org/apache/druid/server/security/CustomCheckX509TrustManager.java b/server/src/main/java/org/apache/druid/server/security/CustomCheckX509TrustManager.java new file mode 100644 index 000000000000..9add4e7cffd0 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/CustomCheckX509TrustManager.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.security; + +import org.apache.druid.java.util.common.logger.Logger; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class CustomCheckX509TrustManager extends X509ExtendedTrustManager implements X509TrustManager +{ + private static final Logger log = new Logger(CustomCheckX509TrustManager.class); + + private final X509ExtendedTrustManager delegate; + private final boolean validateServerHostnames; + private final TLSCertificateChecker certificateChecker; + + public CustomCheckX509TrustManager( + final X509ExtendedTrustManager delegate, + final TLSCertificateChecker certificateChecker, + final boolean validateServerHostnames + ) + { + this.delegate = delegate; + this.validateServerHostnames = validateServerHostnames; + this.certificateChecker = certificateChecker; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + delegate.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + delegate.checkServerTrusted(chain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return delegate.getAcceptedIssuers(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException + { + delegate.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException + { + delegate.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException + { + certificateChecker.checkClient(chain, authType, engine, delegate); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException + { + // The Netty client we use for the internal client does not provide an option to disable the standard hostname + // validation. When using custom certificate checks, we want to allow that option, so we change the endpoint + // identification algorithm here. This is not needed for the server-side, since the Jetty server does provide + // an option for enabling/disabling standard hostname validation. + if (!validateServerHostnames) { + SSLParameters params = engine.getSSLParameters(); + params.setEndpointIdentificationAlgorithm(null); + engine.setSSLParameters(params); + } + + certificateChecker.checkServer(chain, authType, engine, delegate); + } +} diff --git a/server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateChecker.java b/server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateChecker.java new file mode 100644 index 000000000000..4bc2c52752ce --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateChecker.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.security; + +import org.apache.druid.java.util.common.logger.Logger; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class DefaultTLSCertificateChecker implements TLSCertificateChecker +{ + private static final Logger log = new Logger(DefaultTLSCertificateChecker.class); + + @Override + public void checkClient( + X509Certificate[] chain, String authType, SSLEngine engine, X509ExtendedTrustManager baseTrustManager + ) throws CertificateException + { + baseTrustManager.checkClientTrusted(chain, authType, engine); + } + + @Override + public void checkServer( + X509Certificate[] chain, String authType, SSLEngine engine, X509ExtendedTrustManager baseTrustManager + ) throws CertificateException + { + baseTrustManager.checkServerTrusted(chain, authType, engine); + } +} diff --git a/server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateCheckerModule.java b/server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateCheckerModule.java new file mode 100644 index 000000000000..8b2384b2eef4 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/DefaultTLSCertificateCheckerModule.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.security; + +import com.google.inject.Binder; +import com.google.inject.Module; + +import com.google.inject.name.Names; + +public class DefaultTLSCertificateCheckerModule implements Module +{ + private final DefaultTLSCertificateChecker INSTANCE = new DefaultTLSCertificateChecker(); + + public static final String DEFAULT_CHECKER_TYPE = "default"; + + @Override + public void configure(Binder binder) + { + binder.bind(TLSCertificateChecker.class) + .annotatedWith(Names.named(DEFAULT_CHECKER_TYPE)) + .toInstance(INSTANCE); + } +} + diff --git a/server/src/main/java/org/apache/druid/server/security/TLSCertificateChecker.java b/server/src/main/java/org/apache/druid/server/security/TLSCertificateChecker.java new file mode 100644 index 000000000000..5a14861b4f0f --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/TLSCertificateChecker.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.security; + +import org.apache.druid.guice.annotations.ExtensionPoint; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * This extension point allows developers to replace the standard TLS certificate checks with custom checks. + * By default, a {@link DefaultTLSCertificateChecker} is used, which simply delegates to the + * base {@link X509ExtendedTrustManager}. + */ +@ExtensionPoint +public interface TLSCertificateChecker +{ + /** + * This method allows an extension to replace the standard + * {@link X509ExtendedTrustManager#checkClientTrusted(X509Certificate[], String, SSLEngine)} method. + * + * This controls the certificate check used by Druid's server, checking certificates for internal requests made + * by other Druid services and user-submitted requests. + * + * @param chain See docs for {@link X509ExtendedTrustManager#checkClientTrusted(X509Certificate[], String, SSLEngine)}. + * @param authType See docs for {@link X509ExtendedTrustManager#checkClientTrusted(X509Certificate[], String, SSLEngine)}. + * @param engine See docs for {@link X509ExtendedTrustManager#checkClientTrusted(X509Certificate[], String, SSLEngine)}. + * @param baseTrustManager The base trust manager. An extension should call + * baseTrustManager.checkClientTrusted(chain, authType, engine) if/when it wishes + * to use the standard check in addition to custom checks. + * @throws CertificateException + */ + void checkClient( + X509Certificate[] chain, + String authType, + SSLEngine engine, + X509ExtendedTrustManager baseTrustManager + ) throws CertificateException; + + /** + * This method allows an extension to replace the standard + * {@link X509ExtendedTrustManager#checkServerTrusted(X509Certificate[], String, SSLEngine)} method. + * + * This controls the certificate check used by Druid's internal client, used to validate the certificates of other Druid services. + * + * @param chain See docs for {@link X509ExtendedTrustManager#checkServerTrusted(X509Certificate[], String, SSLEngine)}. + * @param authType See docs for {@link X509ExtendedTrustManager#checkServerTrusted(X509Certificate[], String, SSLEngine)}. + * @param engine See docs for {@link X509ExtendedTrustManager#checkServerTrusted(X509Certificate[], String, SSLEngine)}. + * @param baseTrustManager The base trust manager. An extension should call + * baseTrustManager.checkServerTrusted(chain, authType, engine) if/when it wishes + * to use the standard check in addition to custom checks. + * @throws CertificateException + */ + void checkServer( + X509Certificate[] chain, + String authType, + SSLEngine engine, + X509ExtendedTrustManager baseTrustManager + ) throws CertificateException; +} diff --git a/server/src/main/java/org/apache/druid/server/security/TLSCertificateCheckerModule.java b/server/src/main/java/org/apache/druid/server/security/TLSCertificateCheckerModule.java new file mode 100644 index 000000000000..c8a7c9a14946 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/TLSCertificateCheckerModule.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.security; + +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import org.apache.druid.guice.LazySingleton; +import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.ISE; + +import java.util.List; +import java.util.Properties; + +public class TLSCertificateCheckerModule implements Module +{ + private static final String CHECKER_TYPE_PROPERTY = "druid.tls.certificateChecker"; + + private final Properties props; + + @Inject + public TLSCertificateCheckerModule( + Properties props + ) + { + this.props = props; + } + + @Override + public void configure(Binder binder) + { + String checkerType = props.getProperty(CHECKER_TYPE_PROPERTY, DefaultTLSCertificateCheckerModule.DEFAULT_CHECKER_TYPE); + + binder.install(new DefaultTLSCertificateCheckerModule()); + + binder.bind(TLSCertificateChecker.class) + .toProvider(new TLSCertificateCheckerProvider(checkerType)) + .in(LazySingleton.class); + } + + public static class TLSCertificateCheckerProvider implements Provider + { + private final String checkerType; + + private TLSCertificateChecker checker = null; + + public TLSCertificateCheckerProvider( + String checkerType + ) + { + this.checkerType = checkerType; + } + + @Inject + public void inject(Injector injector) + { + final List> checkerBindings = injector.findBindingsByType(new TypeLiteral(){}); + + checker = findChecker(checkerType, checkerBindings); + if (checker == null) { + throw new IAE("Could not find certificate checker with type: " + checkerType); + } + } + + @Override + public TLSCertificateChecker get() + { + if (checker == null) { + throw new ISE("Checker was null, that's bad!"); + } + return checker; + } + + private TLSCertificateChecker findChecker( + String checkerType, + List> checkerBindings + ) + { + for (Binding binding : checkerBindings) { + if (Names.named(checkerType).equals(binding.getKey().getAnnotation())) { + return binding.getProvider().get(); + } + } + return null; + } + } +} diff --git a/server/src/main/java/org/apache/druid/server/security/TLSUtils.java b/server/src/main/java/org/apache/druid/server/security/TLSUtils.java index 691a0ae1c758..948d01285271 100644 --- a/server/src/main/java/org/apache/druid/server/security/TLSUtils.java +++ b/server/src/main/java/org/apache/druid/server/security/TLSUtils.java @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Throwables; +import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.metadata.PasswordProvider; import org.eclipse.jetty.util.ssl.AliasedX509ExtendedKeyManager; @@ -29,8 +30,10 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; import java.io.FileInputStream; import java.io.IOException; import java.security.KeyManagementException; @@ -42,6 +45,8 @@ public class TLSUtils { + private static final Logger log = new Logger(TLSUtils.class); + public static class ClientSSLContextBuilder { private String protocol; @@ -55,6 +60,8 @@ public static class ClientSSLContextBuilder private String certAlias; private PasswordProvider keyStorePasswordProvider; private PasswordProvider keyManagerFactoryPasswordProvider; + private Boolean validateHostnames; + private TLSCertificateChecker certificateChecker; public ClientSSLContextBuilder setProtocol(String protocol) { @@ -122,6 +129,18 @@ public ClientSSLContextBuilder setKeyManagerFactoryPasswordProvider(PasswordProv return this; } + public ClientSSLContextBuilder setValidateHostnames(Boolean validateHostnames) + { + this.validateHostnames = validateHostnames; + return this; + } + + public ClientSSLContextBuilder setCertificateChecker(TLSCertificateChecker certificateChecker) + { + this.certificateChecker = certificateChecker; + return this; + } + public SSLContext build() { Preconditions.checkNotNull(trustStorePath, "must specify a trustStorePath"); @@ -137,7 +156,9 @@ public SSLContext build() keyStoreAlgorithm, certAlias, keyStorePasswordProvider, - keyManagerFactoryPasswordProvider + keyManagerFactoryPasswordProvider, + validateHostnames, + certificateChecker ); } } @@ -153,7 +174,9 @@ public static SSLContext createSSLContext( @Nullable String keyStoreAlgorithm, @Nullable String certAlias, @Nullable PasswordProvider keyStorePasswordProvider, - @Nullable PasswordProvider keyManagerFactoryPasswordProvider + @Nullable PasswordProvider keyManagerFactoryPasswordProvider, + @Nullable Boolean validateHostnames, + TLSCertificateChecker tlsCertificateChecker ) { SSLContext sslContext = null; @@ -171,7 +194,6 @@ public static SSLContext createSSLContext( : trustStoreAlgorithm); trustManagerFactory.init(trustStore); - KeyManager[] keyManagers; if (keyStorePath != null) { KeyStore keyStore = KeyStore.getInstance(keyStoreType == null @@ -195,9 +217,20 @@ public static SSLContext createSSLContext( keyManagers = null; } + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + TrustManager[] newTrustManagers = new TrustManager[trustManagers.length]; + + for (int i = 0; i < trustManagers.length; i++) { + newTrustManagers[i] = new CustomCheckX509TrustManager( + (X509ExtendedTrustManager) trustManagers[i], + tlsCertificateChecker, + validateHostnames == null ? true : validateHostnames + ); + } + sslContext.init( keyManagers, - trustManagerFactory.getTrustManagers(), + newTrustManagers, null ); }