Allow separate truststore conf for HttpEmitter (#5298)

* Fix HttpEmitter TLS support, allow separate truststore conf

* PR comment, fix tests
This commit is contained in:
Jonathan Wei 2018-01-26 08:46:06 -08:00 committed by Himanshu
parent 80419752b5
commit f6749f1229
7 changed files with 273 additions and 34 deletions

View File

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

View File

@ -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<SSLContext>
{
@ -51,25 +43,12 @@ public class SSLContextProvider implements Provider<SSLContext>
{
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()
);
}
}

View File

@ -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<HttpEmitterConfig> config,
Supplier<HttpEmitterSSLClientConfig> 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;
}
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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<ParametrizedUriEmitterConfig> config,
Supplier<ParametrizedUriEmitterSSLClientConfig> 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

View File

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

View File

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