From f6749f122951606f751e33121e0c84b253fcc392 Mon Sep 17 00:00:00 2001 From: Jonathan Wei Date: Fri, 26 Jan 2018 08:46:06 -0800 Subject: [PATCH] Allow separate truststore conf for HttpEmitter (#5298) * Fix HttpEmitter TLS support, allow separate truststore conf * PR comment, fix tests --- docs/content/configuration/index.md | 19 +++- .../io/druid/https/SSLContextProvider.java | 37 ++----- .../server/emitter/HttpEmitterModule.java | 35 ++++++- .../emitter/HttpEmitterSSLClientConfig.java | 98 +++++++++++++++++++ .../emitter/ParametrizedUriEmitterModule.java | 9 +- ...ParametrizedUriEmitterSSLClientConfig.java | 43 ++++++++ .../io/druid/server/security/TLSUtils.java | 66 +++++++++++++ 7 files changed, 273 insertions(+), 34 deletions(-) create mode 100644 server/src/main/java/io/druid/server/emitter/HttpEmitterSSLClientConfig.java create mode 100644 server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterSSLClientConfig.java create mode 100644 server/src/main/java/io/druid/server/security/TLSUtils.java diff --git a/docs/content/configuration/index.md b/docs/content/configuration/index.md index 271a039e362..b0796ace573 100644 --- a/docs/content/configuration/index.md +++ b/docs/content/configuration/index.md @@ -227,11 +227,28 @@ The Druid servers [emit various metrics](../operations/metrics.html) and alerts |`druid.emitter.http.minHttpTimeoutMillis`|If the speed of filling batches imposes timeout smaller than that, not even trying to send batch to endpoint, because it will likely fail, not being able to send the data that fast. Configure this depending based on emitter/successfulSending/minTimeMs metric. Reasonable values are 10ms..100ms.|0| |`druid.emitter.http.recipientBaseUrl`|The base URL to emit messages to. Druid will POST JSON to be consumed at the HTTP endpoint specified by this property.|none, required config| +#### Http Emitter Module TLS Overrides + +When emitting events to a TLS-enabled receiver, the Http Emitter will by default use an SSLContext obtained via the process described at [Druid's internal communication over TLS](../operations/tls-support.html#druids-internal-communication-over-tls), i.e., the same SSLContext that would be used for internal communications between Druid nodes. + +In some use cases it may be desirable to have the Http Emitter use its own separate truststore configuration. For example, there may be organizational policies that prevent the TLS-enabled metrics receiver's certificate from being added to the same truststore used by Druid's internal HTTP client. + +The following properties allow the Http Emitter to use its own truststore configuration when building its SSLContext. + +|Property|Description|Default| +|--------|-----------|-------| +|`druid.emitter.http.ssl.useDefaultJavaContext`|If set to true, the HttpEmitter will use `SSLContext.getDefault()`, the default Java SSLContext, and all other properties below are ignored.|false| +|`druid.emitter.http.ssl.trustStorePath`|The file path or URL of the TLS/SSL Key store where trusted root certificates are stored. If this is unspecified, the Http Emitter will use the same SSLContext as Druid's internal HTTP client, as described in the beginning of this section, and all other properties below are ignored.|null| +|`druid.emitter.http.ssl.trustStoreType`|The type of the key store where trusted root certificates are stored.|`java.security.KeyStore.getDefaultType()`| +|`druid.emitter.http.ssl.trustStoreAlgorithm`|Algorithm to be used by TrustManager to validate certificate chains|`javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()`| +|`druid.emitter.http.ssl.trustStorePassword`|The [Password Provider](../../operations/password-provider.html) or String password for the Trust Store.|none| +|`druid.emitter.http.ssl.protocol`|TLS protocol to use.|"TLSv1.2"| + #### Parametrized Http Emitter Module `druid.emitter.parametrized.httpEmitting.*` configs correspond to the configs of Http Emitter Modules, see above. Except `recipientBaseUrl`. E. g. `druid.emitter.parametrized.httpEmitting.flushMillis`, -`druid.emitter.parametrized.httpEmitting.flushCount`, etc. +`druid.emitter.parametrized.httpEmitting.flushCount`, `druid.emitter.parametrized.httpEmitting.ssl.trustStorePath`, etc. The additional configs are: diff --git a/extensions-core/simple-client-sslcontext/src/main/java/io/druid/https/SSLContextProvider.java b/extensions-core/simple-client-sslcontext/src/main/java/io/druid/https/SSLContextProvider.java index ee53c9278e0..46345c4b188 100644 --- a/extensions-core/simple-client-sslcontext/src/main/java/io/druid/https/SSLContextProvider.java +++ b/extensions-core/simple-client-sslcontext/src/main/java/io/druid/https/SSLContextProvider.java @@ -19,20 +19,12 @@ package io.druid.https; -import com.google.common.base.Throwables; import com.google.inject.Inject; import com.google.inject.Provider; import io.druid.java.util.emitter.EmittingLogger; +import io.druid.server.security.TLSUtils; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.io.FileInputStream; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; public class SSLContextProvider implements Provider { @@ -51,25 +43,12 @@ public class SSLContextProvider implements Provider { log.info("Creating SslContext for https client using config [%s]", config); - SSLContext sslContext = null; - try { - sslContext = SSLContext.getInstance(config.getProtocol() == null ? "TLSv1.2" : config.getProtocol()); - KeyStore keyStore = KeyStore.getInstance(config.getTrustStoreType() == null - ? KeyStore.getDefaultType() - : config.getTrustStoreType()); - keyStore.load( - new FileInputStream(config.getTrustStorePath()), - config.getTrustStorePasswordProvider().getPassword().toCharArray() - ); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(config.getTrustStoreAlgorithm() == null - ? TrustManagerFactory.getDefaultAlgorithm() - : config.getTrustStoreAlgorithm()); - trustManagerFactory.init(keyStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - } - catch (CertificateException | KeyManagementException | IOException | KeyStoreException | NoSuchAlgorithmException e) { - Throwables.propagate(e); - } - return sslContext; + return TLSUtils.createSSLContext( + config.getProtocol(), + config.getTrustStoreType(), + config.getTrustStorePath(), + config.getTrustStoreAlgorithm(), + config.getTrustStorePasswordProvider() + ); } } diff --git a/server/src/main/java/io/druid/server/emitter/HttpEmitterModule.java b/server/src/main/java/io/druid/server/emitter/HttpEmitterModule.java index b09ba21d63f..7404a348c5b 100644 --- a/server/src/main/java/io/druid/server/emitter/HttpEmitterModule.java +++ b/server/src/main/java/io/druid/server/emitter/HttpEmitterModule.java @@ -26,14 +26,16 @@ import com.google.inject.Module; import com.google.inject.Provides; import com.google.inject.name.Named; import com.google.inject.util.Providers; +import io.druid.guice.LazySingleton; +import io.druid.java.util.common.logger.Logger; import io.druid.java.util.emitter.core.Emitter; import io.druid.java.util.emitter.core.HttpEmitterConfig; import io.druid.java.util.emitter.core.HttpPostEmitter; import io.druid.guice.JsonConfigProvider; -import io.druid.guice.LazySingleton; import io.druid.guice.ManageLifecycle; import io.druid.java.util.common.concurrent.Execs; import io.druid.java.util.common.lifecycle.Lifecycle; +import io.druid.server.security.TLSUtils; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.JdkSslContext; import io.netty.util.HashedWheelTimer; @@ -49,10 +51,13 @@ import java.security.NoSuchAlgorithmException; */ public class HttpEmitterModule implements Module { + private static final Logger log = new Logger(HttpEmitterModule.class); + @Override public void configure(Binder binder) { JsonConfigProvider.bind(binder, "druid.emitter.http", HttpEmitterConfig.class); + JsonConfigProvider.bind(binder, "druid.emitter.http.ssl", HttpEmitterSSLClientConfig.class); configureSsl(binder); } @@ -90,6 +95,7 @@ public class HttpEmitterModule implements Module @Named("http") public Emitter getEmitter( Supplier config, + Supplier sslConfig, @Nullable SSLContext sslContext, Lifecycle lifecycle, ObjectMapper jsonMapper @@ -101,10 +107,35 @@ public class HttpEmitterModule implements Module createAsyncHttpClient( "HttpPostEmitter-AsyncHttpClient-%d", "HttpPostEmitter-AsyncHttpClient-Timer-%d", - sslContext + getEffectiveSSLContext(sslConfig.get(), sslContext) ) ), jsonMapper ); } + + public static SSLContext getEffectiveSSLContext(HttpEmitterSSLClientConfig sslConfig, SSLContext sslContext) + { + SSLContext effectiveSSLContext; + if (sslConfig.isUseDefaultJavaContext()) { + try { + effectiveSSLContext = SSLContext.getDefault(); + } + catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException(nsae); + } + } else if (sslConfig.getTrustStorePath() != null) { + log.info("Creating SSLContext for HttpEmitter client using config [%s]", sslConfig); + effectiveSSLContext = TLSUtils.createSSLContext( + sslConfig.getProtocol(), + sslConfig.getTrustStoreType(), + sslConfig.getTrustStorePath(), + sslConfig.getTrustStoreAlgorithm(), + sslConfig.getTrustStorePasswordProvider() + ); + } else { + effectiveSSLContext = sslContext; + } + return effectiveSSLContext; + } } diff --git a/server/src/main/java/io/druid/server/emitter/HttpEmitterSSLClientConfig.java b/server/src/main/java/io/druid/server/emitter/HttpEmitterSSLClientConfig.java new file mode 100644 index 00000000000..0086160e902 --- /dev/null +++ b/server/src/main/java/io/druid/server/emitter/HttpEmitterSSLClientConfig.java @@ -0,0 +1,98 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.server.emitter; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.druid.metadata.PasswordProvider; + +/** + * This is kept separate from {@link io.druid.java.util.emitter.core.HttpEmitterConfig} because {@link PasswordProvider} + * is currently located in druid-api. The java-util module which contains HttpEmitterConfig cannot import + * PasswordProvider because this would introduce a circular dependence between java-util and druid-api. + * + * PasswordProvider could be moved to java-util, but PasswordProvider is annotated with + * {@link io.druid.guice.annotations.ExtensionPoint}, which would also have to be moved. + * + * It would be easier to resolve these issues and merge the TLS-related config with HttpEmitterConfig once + * https://github.com/druid-io/druid/issues/4312 is resolved, so the TLS config is kept separate for now. + */ +public class HttpEmitterSSLClientConfig +{ + @JsonProperty + private String protocol; + + @JsonProperty + private String trustStoreType; + + @JsonProperty + private String trustStorePath; + + @JsonProperty + private String trustStoreAlgorithm; + + @JsonProperty("trustStorePassword") + private PasswordProvider trustStorePasswordProvider; + + @JsonProperty("useDefaultJavaContext") + private boolean useDefaultJavaContext = false; + + public String getProtocol() + { + return protocol; + } + + public String getTrustStoreType() + { + return trustStoreType; + } + + public String getTrustStorePath() + { + return trustStorePath; + } + + public String getTrustStoreAlgorithm() + { + return trustStoreAlgorithm; + } + + public PasswordProvider getTrustStorePasswordProvider() + { + return trustStorePasswordProvider; + } + + public boolean isUseDefaultJavaContext() + { + return useDefaultJavaContext; + } + + @Override + public String toString() + { + return "HttpEmitterSSLClientConfig{" + + "protocol='" + protocol + '\'' + + ", trustStoreType='" + trustStoreType + '\'' + + ", trustStorePath='" + trustStorePath + '\'' + + ", trustStoreAlgorithm='" + trustStoreAlgorithm + '\'' + + ", useDefaultJavaContext='" + useDefaultJavaContext + '\'' + + '}'; + } +} diff --git a/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterModule.java b/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterModule.java index 3ea1f672eb9..e465149779c 100644 --- a/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterModule.java +++ b/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterModule.java @@ -25,6 +25,7 @@ import com.google.inject.Binder; import com.google.inject.Module; import com.google.inject.Provides; import com.google.inject.name.Named; +import io.druid.java.util.common.logger.Logger; import io.druid.java.util.emitter.core.Emitter; import io.druid.java.util.emitter.core.ParametrizedUriEmitter; import io.druid.java.util.emitter.core.ParametrizedUriEmitterConfig; @@ -37,11 +38,13 @@ import javax.net.ssl.SSLContext; public class ParametrizedUriEmitterModule implements Module { + private static final Logger log = new Logger(ParametrizedUriEmitterModule.class); + @Override public void configure(Binder binder) { JsonConfigProvider.bind(binder, "druid.emitter.parametrized", ParametrizedUriEmitterConfig.class); - HttpEmitterModule.configureSsl(binder); + JsonConfigProvider.bind(binder, "druid.emitter.parametrized.httpEmitting", ParametrizedUriEmitterSSLClientConfig.class); } @Provides @@ -49,18 +52,20 @@ public class ParametrizedUriEmitterModule implements Module @Named("parametrized") public Emitter getEmitter( Supplier config, + Supplier parametrizedSSLClientConfig, @Nullable SSLContext sslContext, Lifecycle lifecycle, ObjectMapper jsonMapper ) { + HttpEmitterSSLClientConfig sslConfig = parametrizedSSLClientConfig.get().getHttpEmittingSSLClientConfig(); return new ParametrizedUriEmitter( config.get(), lifecycle.addCloseableInstance( HttpEmitterModule.createAsyncHttpClient( "ParmetrizedUriEmitter-AsyncHttpClient-%d", "ParmetrizedUriEmitter-AsyncHttpClient-Timer-%d", - sslContext + HttpEmitterModule.getEffectiveSSLContext(sslConfig, sslContext) ) ), jsonMapper diff --git a/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterSSLClientConfig.java b/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterSSLClientConfig.java new file mode 100644 index 00000000000..577b583e7fb --- /dev/null +++ b/server/src/main/java/io/druid/server/emitter/ParametrizedUriEmitterSSLClientConfig.java @@ -0,0 +1,43 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.server.emitter; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ParametrizedUriEmitterSSLClientConfig +{ + private static final HttpEmitterSSLClientConfig HTTP_EMITTER_SSL_CLIENT_CONFIG = new HttpEmitterSSLClientConfig(); + + @JsonProperty("ssl") + private HttpEmitterSSLClientConfig httpEmittingSSLClientConfig = HTTP_EMITTER_SSL_CLIENT_CONFIG; + + @Override + public String toString() + { + return "ParametrizedUriEmitterSSLClientConfig{" + + "httpEmittingSSLClientConfig='" + httpEmittingSSLClientConfig + + '}'; + } + + public HttpEmitterSSLClientConfig getHttpEmittingSSLClientConfig() + { + return httpEmittingSSLClientConfig; + } +} diff --git a/server/src/main/java/io/druid/server/security/TLSUtils.java b/server/src/main/java/io/druid/server/security/TLSUtils.java new file mode 100644 index 00000000000..db58817cc93 --- /dev/null +++ b/server/src/main/java/io/druid/server/security/TLSUtils.java @@ -0,0 +1,66 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.server.security; + +import com.google.common.base.Throwables; +import io.druid.metadata.PasswordProvider; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class TLSUtils +{ + public static SSLContext createSSLContext( + String protocol, + String trustStoreType, + String trustStorePath, + String trustStoreAlgorithm, + PasswordProvider trustStorePasswordProvider + ) + { + SSLContext sslContext = null; + try { + sslContext = SSLContext.getInstance(protocol == null ? "TLSv1.2" : protocol); + KeyStore keyStore = KeyStore.getInstance(trustStoreType == null + ? KeyStore.getDefaultType() + : trustStoreType); + keyStore.load( + new FileInputStream(trustStorePath), + trustStorePasswordProvider.getPassword().toCharArray() + ); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm == null + ? TrustManagerFactory.getDefaultAlgorithm() + : trustStoreAlgorithm); + trustManagerFactory.init(keyStore); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + } + catch (CertificateException | KeyManagementException | IOException | KeyStoreException | NoSuchAlgorithmException e) { + Throwables.propagate(e); + } + return sslContext; + } +}