Allow custom TLS cert checks (#6432)

* Allow custom TLS cert checks

* PR comment

* Checkstyle, PR comment
This commit is contained in:
Jonathan Wei 2018-10-24 16:31:52 -07:00 committed by Jihoon Son
parent 601183b4c7
commit b2d9b6f23d
25 changed files with 810 additions and 24 deletions

View File

@ -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.

View File

@ -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. Please refer to extension documentation for the type name that should be specified.|"default"|no|
The default checker delegates to the standard 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.

View File

@ -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 class SSLClientConfig
return keyManagerFactoryAlgorithm;
}
public Boolean getValidateHostnames()
{
return validateHostnames;
}
@Override
public String toString()
{
@ -124,6 +132,7 @@ public class SSLClientConfig
", keyStoreType='" + keyStoreType + '\'' +
", certAlias='" + certAlias + '\'' +
", keyManagerFactoryAlgorithm='" + keyManagerFactoryAlgorithm + '\'' +
", validateHostnames='" + validateHostnames + '\'' +
'}';
}
}

View File

@ -22,6 +22,7 @@ package org.apache.druid.https;
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<SSLContext>
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 class SSLContextProvider implements Provider<SSLContext>
.setCertAlias(config.getCertAlias())
.setKeyStorePasswordProvider(config.getKeyStorePasswordProvider())
.setKeyManagerFactoryPasswordProvider(config.getKeyManagerPasswordProvider())
.setValidateHostnames(config.getValidateHostnames())
.setCertificateChecker(certificateChecker)
.build();
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
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 class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
return noClientAuthRouterTLSUrl;
}
@Override
public String getCustomCertCheckRouterUrl()
{
return customCertCheckRouterUrl;
}
@Override
public String getCustomCertCheckRouterTLSUrl()
{
return customCertCheckRouterTLSUrl;
}
@Override
public String getBrokerUrl()
{

View File

@ -102,6 +102,18 @@ public class DockerConfigProvider implements IntegrationTestingConfigProvider
return "https://" + dockerIp + ":9090";
}
@Override
public String getCustomCertCheckRouterUrl()
{
return "http://" + dockerIp + ":8891";
}
@Override
public String getCustomCertCheckRouterTLSUrl()
{
return "https://" + dockerIp + ":9091";
}
@Override
public String getBrokerUrl()
{

View File

@ -45,6 +45,10 @@ public interface IntegrationTestingConfig
String getNoClientAuthRouterTLSUrl();
String getCustomCertCheckRouterUrl();
String getCustomCertCheckRouterTLSUrl();
String getBrokerUrl();
String getBrokerTLSUrl();

View File

@ -59,5 +59,4 @@ public class DruidTestModuleFactory implements IModuleFactory
context.addInjector(Collections.singletonList(module), injector);
return module;
}
}

View File

@ -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<? extends Module> getJacksonModules()
{
return Collections.EMPTY_LIST;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.");
}
}
}

View File

@ -0,0 +1 @@
org.apache.druid.testing.guice.ITTLSCertificateCheckerModule

View File

@ -38,9 +38,11 @@ import org.apache.druid.java.util.http.client.Request;
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 class ITTLSTest
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 @@ public class ITTLSTest
url + "/status",
"Certless",
SSLException.class,
"Received fatal alert: bad_certificate"
"Received fatal alert: bad_certificate",
false
);
}
@ -254,7 +287,8 @@ public class ITTLSTest
url + "/status",
"Wrong hostname",
SSLException.class,
"Received fatal alert: certificate_unknown"
"Received fatal alert: certificate_unknown",
false
);
}
@ -266,7 +300,8 @@ public class ITTLSTest
url + "/status",
"Wrong root cert",
SSLException.class,
"Received fatal alert: certificate_unknown"
"Received fatal alert: certificate_unknown",
false
);
}
@ -278,7 +313,8 @@ public class ITTLSTest
url + "/status",
"Revoked cert",
SSLException.class,
"Received fatal alert: certificate_unknown"
"Received fatal alert: certificate_unknown",
false
);
}
@ -290,7 +326,8 @@ public class ITTLSTest
url + "/status",
"Expired cert",
SSLException.class,
"Received fatal alert: certificate_unknown"
"Received fatal alert: certificate_unknown",
false
);
}
@ -302,7 +339,8 @@ public class ITTLSTest
url + "/status",
"Cert signed by non-CA",
SSLException.class,
"Received fatal alert: certificate_unknown"
"Received fatal alert: certificate_unknown",
false
);
}
@ -322,6 +360,15 @@ public class ITTLSTest
}
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 @@ public class ITTLSTest
.setCertAlias(certAlias)
.setKeyStorePasswordProvider(sslClientConfig.getKeyStorePasswordProvider())
.setKeyManagerFactoryPasswordProvider(sslClientConfig.getKeyManagerPasswordProvider())
.setCertificateChecker(certificateChecker)
.build();
final HttpClientConfig.Builder builder = getHttpClientConfigBuilder(intermediateClientSSLContext);
@ -361,6 +409,7 @@ public class ITTLSTest
.setTrustStorePath(sslClientConfig.getTrustStorePath())
.setTrustStoreAlgorithm(sslClientConfig.getTrustStoreAlgorithm())
.setTrustStorePasswordProvider(sslClientConfig.getTrustStorePasswordProvider())
.setCertificateChecker(certificateChecker)
.build();
final HttpClientConfig.Builder builder = getHttpClientConfigBuilder(certlessClientSSLContext);
@ -385,7 +434,8 @@ public class ITTLSTest
String url,
String clientDesc,
Class expectedException,
String expectedExceptionMsg
String expectedExceptionMsg,
boolean useContainsMsgCheck
)
{
int retries = 0;
@ -411,13 +461,17 @@ public class ITTLSTest
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;

View File

@ -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

View File

@ -70,6 +70,7 @@ import org.apache.druid.server.initialization.AuthenticatorMapperModule;
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 class Initialization
new Log4jShutterDownerModule(),
new DruidAuthModule(),
new LifecycleModule(),
TLSCertificateCheckerModule.class,
EmitterModule.class,
HttpClientModule.global(),
HttpClientModule.escalatedGlobal(),

View File

@ -36,6 +36,7 @@ import org.apache.druid.server.DruidNode;
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 class ChatHandlerServerModule implements Module
node,
config,
TLSServerConfig,
injector.getExistingBinding(Key.get(SslContextFactory.class))
injector.getExistingBinding(Key.get(SslContextFactory.class)),
injector.getInstance(TLSCertificateChecker.class)
);
}
}

View File

@ -60,6 +60,8 @@ import org.apache.druid.server.initialization.TLSServerConfig;
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 org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
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 class JettyServerModule extends JerseyServletModule
node,
config,
TLSServerConfig,
injector.getExistingBinding(Key.get(SslContextFactory.class))
injector.getExistingBinding(Key.get(SslContextFactory.class)),
injector.getInstance(TLSCertificateChecker.class)
);
}
@ -187,7 +194,8 @@ public class JettyServerModule extends JerseyServletModule
DruidNode node,
ServerConfig config,
TLSServerConfig tlsServerConfig,
Binding<SslContextFactory> sslContextFactoryBinding
Binding<SslContextFactory> sslContextFactoryBinding,
TLSCertificateChecker certificateChecker
)
{
// adjusting to make config.getNumThreads() mean, "number of threads
@ -235,7 +243,7 @@ public class JettyServerModule extends JerseyServletModule
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,45 @@ public class JettyServerModule extends JerseyServletModule
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<? extends CRL> 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()
);
} else {
newTrustManagers[i] = trustManagers[i];
log.info("Encountered non-X509ExtendedTrustManager: " + trustManagers[i].getClass());
}
}
return newTrustManagers;
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,54 @@
/*
* 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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<TLSCertificateChecker>
{
private final String checkerType;
private TLSCertificateChecker checker = null;
public TLSCertificateCheckerProvider(
String checkerType
)
{
this.checkerType = checkerType;
}
@Inject
public void inject(Injector injector)
{
final List<Binding<TLSCertificateChecker>> checkerBindings = injector.findBindingsByType(new TypeLiteral<TLSCertificateChecker>(){});
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<Binding<TLSCertificateChecker>> checkerBindings
)
{
for (Binding<TLSCertificateChecker> binding : checkerBindings) {
if (Names.named(checkerType).equals(binding.getKey().getAnnotation())) {
return binding.getProvider().get();
}
}
return null;
}
}
}

View File

@ -21,6 +21,7 @@ package org.apache.druid.server.security;
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.annotation.Nullable;
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 @@ import java.security.cert.CertificateException;
public class TLSUtils
{
private static final Logger log = new Logger(TLSUtils.class);
public static class ClientSSLContextBuilder
{
private String protocol;
@ -55,6 +60,8 @@ public class TLSUtils
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 class TLSUtils
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 class TLSUtils
keyStoreAlgorithm,
certAlias,
keyStorePasswordProvider,
keyManagerFactoryPasswordProvider
keyManagerFactoryPasswordProvider,
validateHostnames,
certificateChecker
);
}
}
@ -153,7 +174,9 @@ public class TLSUtils
@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 class TLSUtils
: trustStoreAlgorithm);
trustManagerFactory.init(trustStore);
KeyManager[] keyManagers;
if (keyStorePath != null) {
KeyStore keyStore = KeyStore.getInstance(keyStoreType == null
@ -195,9 +217,25 @@ public class TLSUtils
keyManagers = null;
}
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
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],
tlsCertificateChecker,
validateHostnames == null ? true : validateHostnames
);
} else {
newTrustManagers[i] = trustManagers[i];
log.info("Encountered non-X509ExtendedTrustManager: " + trustManagers[i].getClass());
}
}
sslContext.init(
keyManagers,
trustManagerFactory.getTrustManagers(),
newTrustManagers,
null
);
}