[ARTEMIS-2704]: Provide a SPI to manage and cache SSLContext.

* Adding a new SPI to allow for SSLContext reuse accross the broker.
 * Providing a default behaviour similar to the existing one.

[ARTEMIS-2718]: Take advantage of ARTEMIS-2704 to cache SSLContexts.
* Adding a cache for SSLContexts and reusing them accross acceptors and
  connectors.

Issue: https://issues.apache.org/jira/browse/ARTEMIS-2704
Issue: https://issues.apache.org/jira/browse/ARTEMIS-2718
This commit is contained in:
Emmanuel Hugonnet 2020-05-14 14:08:09 +02:00 committed by Clebert Suconic
parent 79fef2f20b
commit a88815d9b3
10 changed files with 319 additions and 36 deletions

View File

@ -103,7 +103,6 @@ import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetector.Level; import io.netty.util.ResourceLeakDetector.Level;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.GlobalEventExecutor;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.client.ActiveMQClientLogger; import org.apache.activemq.artemis.core.client.ActiveMQClientLogger;
import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle; import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle;
@ -123,6 +122,9 @@ import org.jboss.logging.Logger;
import static org.apache.activemq.artemis.utils.Base64.encodeBytes; import static org.apache.activemq.artemis.utils.Base64.encodeBytes;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider;
public class NettyConnector extends AbstractConnector { public class NettyConnector extends AbstractConnector {
public static String NIO_CONNECTOR_TYPE = "NIO"; public static String NIO_CONNECTOR_TYPE = "NIO";
@ -674,22 +676,10 @@ public class NettyConnector extends AbstractConnector {
String truststoreProvider, String truststoreProvider,
String truststorePath, String truststorePath,
String truststorePassword) throws Exception { String truststorePassword) throws Exception {
SSLContext context; SSLContext context = SSLContextFactoryProvider.getSSLContextFactory().getSSLContext(configuration,
if (useDefaultSslContext) { keystoreProvider, keystorePath, keystorePassword,
context = SSLContext.getDefault(); truststoreProvider, truststorePath, truststorePassword,
} else { crlPath, trustManagerFactoryPlugin, trustAll);
context = new SSLSupport()
.setKeystoreProvider(keystoreProvider)
.setKeystorePath(keystorePath)
.setKeystorePassword(keystorePassword)
.setTruststoreProvider(truststoreProvider)
.setTruststorePath(truststorePath)
.setTruststorePassword(truststorePassword)
.setTrustAll(trustAll)
.setCrlPath(crlPath)
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createContext();
}
Subject subject = null; Subject subject = null;
if (kerb5Config != null) { if (kerb5Config != null) {
LoginContext loginContext = new LoginContext(kerb5Config); LoginContext loginContext = new LoginContext(kerb5Config);

View File

@ -29,6 +29,8 @@ public class TransportConstants {
private static final Logger logger = Logger.getLogger(TransportConstants.class); private static final Logger logger = Logger.getLogger(TransportConstants.class);
public static final String SSL_CONTEXT_PROP_NAME = "sslContext";
public static final String SSL_ENABLED_PROP_NAME = "sslEnabled"; public static final String SSL_ENABLED_PROP_NAME = "sslEnabled";
public static final String SSL_KRB5_CONFIG_PROP_NAME = "sslKrb5Config"; public static final String SSL_KRB5_CONFIG_PROP_NAME = "sslKrb5Config";

View File

@ -0,0 +1,84 @@
/*
* Copyright 2020 The Apache Software Foundation.
*
* Licensed 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.activemq.artemis.core.remoting.impl.ssl;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
/**
* SSLContextFactory providing a cache of SSLContext.
* Since SSLContext should be reused instead of recreated and are thread safe.
* To activate it uou need to allow this Service to be discovered by having a
* <code>META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory</code>
* file with <code> org.apache.activemq.artemis.core.remoting.impl.ssl.CachingSSLContextFactory</code>
* as value.
*/
public class CachingSSLContextFactory extends DefaultSSLContextFactory {
private static final Map<String, SSLContext> SSL_CONTEXTS = Collections.synchronizedMap(new HashMap<>());
@Override
public void clearSSLContexts() {
SSL_CONTEXTS.clear();
}
@Override
public SSLContext getSSLContext(Map<String, Object> configuration,
String keystoreProvider, String keystorePath, String keystorePassword,
String truststoreProvider, String truststorePath, String truststorePassword,
String crlPath, String trustManagerFactoryPlugin, boolean trustAll) throws Exception {
String sslContextName = getSSLContextName(configuration, keystorePath, keystoreProvider, truststorePath, truststoreProvider);
if (!SSL_CONTEXTS.containsKey(sslContextName)) {
SSL_CONTEXTS.put(sslContextName, createSSLContext(configuration,
keystoreProvider, keystorePath, keystorePassword,
truststoreProvider, truststorePath, truststorePassword,
crlPath, trustManagerFactoryPlugin, trustAll));
}
return SSL_CONTEXTS.get(sslContextName);
}
/**
* Obtain the sslContextName :
* - if available the 'sslContext' from the configuration
* - otherwise if available the keyStorePath + '_' + keystoreProvider
* - otherwise the truststorePath + '_' + truststoreProvider.
* @param configuration
* @param keyStorePath
* @param keystoreProvider
* @param truststorePath
* @param truststoreProvider
* @return the ley associated to the SSLContext.
*/
protected String getSSLContextName(Map<String, Object> configuration, String keyStorePath, String keystoreProvider, String truststorePath, String truststoreProvider) {
String sslContextName = ConfigurationHelper.getStringProperty(TransportConstants.SSL_CONTEXT_PROP_NAME, null, configuration);
if (sslContextName == null) {
if (keyStorePath != null) {
return keyStorePath + '_' + keystoreProvider;
}
return truststorePath + '_' + truststoreProvider;
}
return sslContextName;
}
@Override
public int getPriority() {
return 10;
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2020 The Apache Software Foundation.
*
* Licensed 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.activemq.artemis.core.remoting.impl.ssl;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
/**
* Simple SSLContextFactory for use in NettyConnector and NettyAcceptor.
*/
public class DefaultSSLContextFactory implements SSLContextFactory {
@Override
public SSLContext getSSLContext(Map<String, Object> configuration,
String keystoreProvider, String keystorePath, String keystorePassword,
String truststoreProvider, String truststorePath, String truststorePassword,
String crlPath, String trustManagerFactoryPlugin, boolean trustAll) throws Exception {
return createSSLContext(configuration,
keystoreProvider, keystorePath, keystorePassword,
truststoreProvider, truststorePath, truststorePassword,
crlPath, trustManagerFactoryPlugin, trustAll);
}
protected SSLContext createSSLContext(Map<String, Object> configuration,
String keystoreProvider, String keystorePath, String keystorePassword,
String truststoreProvider, String truststorePath, String truststorePassword,
String crlPath, String trustManagerFactoryPlugin, boolean trustAll) throws Exception {
if (log.isDebugEnabled()) {
final StringBuilder builder = new StringBuilder();
configuration.forEach((k, v) -> builder.append("\r\n").append(k).append("=").append(v));
log.debugf("Creating SSL context with configuration %s", builder.toString());
}
boolean useDefaultSslContext = ConfigurationHelper.getBooleanProperty(TransportConstants.USE_DEFAULT_SSL_CONTEXT_PROP_NAME, TransportConstants.DEFAULT_USE_DEFAULT_SSL_CONTEXT, configuration);
if (useDefaultSslContext) {
return SSLContext.getDefault();
}
return new SSLSupport()
.setKeystoreProvider(keystoreProvider)
.setKeystorePath(keystorePath)
.setKeystorePassword(keystorePassword)
.setTruststoreProvider(truststoreProvider)
.setTruststorePath(truststorePath)
.setTruststorePassword(truststorePassword)
.setTrustAll(trustAll)
.setCrlPath(crlPath)
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createContext();
}
@Override
public int getPriority() {
return 5;
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2020 The Apache Software Foundation.
*
* Licensed 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.activemq.artemis.spi.core.remoting.ssl;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.jboss.logging.Logger;
/**
* Service interface to create a SSLContext for a configuration.
* This is NOT used by OpenSSL.
* To create and use your own implementation you need to create a file
* <code>META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory</code>
* in your jar and fill it with the full qualified name of your implementation.
*/
public interface SSLContextFactory extends Comparable<SSLContextFactory> {
Logger log = Logger.getLogger(SSLContextFactory.class);
/**
* Obtain a SSLContext from the configuration.
* @param configuration
* @param keystoreProvider
* @param keystorePath
* @param keystorePassword
* @param truststoreProvider
* @param truststorePath
* @param truststorePassword
* @param crlPath
* @param trustManagerFactoryPlugin
* @param trustAll
* @return a SSLContext instance.
* @throws Exception
*/
SSLContext getSSLContext(Map<String, Object> configuration,
String keystoreProvider, String keystorePath, String keystorePassword,
String truststoreProvider, String truststorePath, String truststorePassword,
String crlPath, String trustManagerFactoryPlugin, boolean trustAll) throws Exception;
default void clearSSLContexts() {
}
/**
* The priority for the SSLContextFactory when resolving the service to get the implementation.
* This is used when selecting the implementation when several implementations are loaded.
* The highest priority implementation will be used.
* @return the priority.
*/
int getPriority();
@Override
default int compareTo(SSLContextFactory other) {
return this.getPriority() - other.getPriority();
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2020 The Apache Software Foundation.
*
* Licensed 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.activemq.artemis.spi.core.remoting.ssl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
/**
* Provider that loads the SSLContextFactory services and return the one with the highest priority.
* This is only used to provide SSLContext, so it doesn't support OpenSSL.
*/
public class SSLContextFactoryProvider {
private static final SSLContextFactory factory;
static {
ServiceLoader<SSLContextFactory> loader = ServiceLoader.load(SSLContextFactory.class, Thread.currentThread().getContextClassLoader());
final List<SSLContextFactory> factories = new ArrayList<>();
loader.forEach(factories::add);
Collections.sort(factories);
factory = factories.get(factories.size() - 1);
}
/**
* @return the SSLContextFactory with the higher priority.
*/
public static SSLContextFactory getSSLContextFactory() {
return factory;
}
}

View File

@ -0,0 +1 @@
org.apache.activemq.artemis.core.remoting.impl.ssl.DefaultSSLContextFactory

View File

@ -92,6 +92,7 @@ import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager;
import org.apache.activemq.artemis.spi.core.remoting.BufferHandler; import org.apache.activemq.artemis.spi.core.remoting.BufferHandler;
import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener; import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener;
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider;
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory; import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
import org.apache.activemq.artemis.utils.ConfigurationHelper; import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.collections.TypedProperties; import org.apache.activemq.artemis.utils.collections.TypedProperties;
@ -489,6 +490,7 @@ public class NettyAcceptor extends AbstractAcceptor {
// only for testing purposes // only for testing purposes
public void setKeyStorePath(String keyStorePath) { public void setKeyStorePath(String keyStorePath) {
this.keyStorePath = keyStorePath; this.keyStorePath = keyStorePath;
this.configuration.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, keyStorePath);
} }
/** /**
@ -519,7 +521,7 @@ public class NettyAcceptor extends AbstractAcceptor {
public synchronized SslHandler getSslHandler(ByteBufAllocator alloc, String peerHost, int peerPort) throws Exception { public synchronized SslHandler getSslHandler(ByteBufAllocator alloc, String peerHost, int peerPort) throws Exception {
SSLEngine engine; SSLEngine engine;
if (sslProvider.equals(TransportConstants.OPENSSL_PROVIDER)) { if (TransportConstants.OPENSSL_PROVIDER.equals(sslProvider)) {
engine = loadOpenSslEngine(alloc, peerHost, peerPort); engine = loadOpenSslEngine(alloc, peerHost, peerPort);
} else { } else {
engine = loadJdkSslEngine(peerHost, peerPort); engine = loadJdkSslEngine(peerHost, peerPort);
@ -594,21 +596,13 @@ public class NettyAcceptor extends AbstractAcceptor {
private SSLEngine loadJdkSslEngine(String peerHost, int peerPort) throws Exception { private SSLEngine loadJdkSslEngine(String peerHost, int peerPort) throws Exception {
final SSLContext context; final SSLContext context;
try { try {
if (kerb5Config == null && keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider)) checkSSLConfiguration();
throw new IllegalArgumentException("If \"" + TransportConstants.SSL_ENABLED_PROP_NAME + "\" is true then \"" + TransportConstants.KEYSTORE_PATH_PROP_NAME + "\" must be non-null " + "unless an alternative \"" + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME + "\" has been specified."); context = SSLContextFactoryProvider.getSSLContextFactory().getSSLContext(configuration,
context = new SSLSupport() keyStoreProvider, keyStorePath, keyStorePassword,
.setKeystoreProvider(keyStoreProvider) trustStoreProvider, trustStorePath, trustStorePassword,
.setKeystorePath(keyStorePath) crlPath, trustManagerFactoryPlugin, TransportConstants.DEFAULT_TRUST_ALL);
.setKeystorePassword(keyStorePassword)
.setTruststoreProvider(trustStoreProvider)
.setTruststorePath(trustStorePath)
.setTruststorePassword(trustStorePassword)
.setCrlPath(crlPath)
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createContext();
} catch (Exception e) { } catch (Exception e) {
IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port); IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port, e);
ise.initCause(e);
throw ise; throw ise;
} }
Subject subject = null; Subject subject = null;
@ -631,11 +625,19 @@ public class NettyAcceptor extends AbstractAcceptor {
return engine; return engine;
} }
private void checkSSLConfiguration() throws IllegalArgumentException {
if (configuration.containsKey(TransportConstants.SSL_CONTEXT_PROP_NAME)) {
return;
}
if (kerb5Config == null && keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider)) {
throw new IllegalArgumentException("If \"" + TransportConstants.SSL_ENABLED_PROP_NAME + "\" is true then \"" + TransportConstants.KEYSTORE_PATH_PROP_NAME + "\" must be non-null " + "unless an alternative \"" + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME + "\" has been specified.");
}
}
private SSLEngine loadOpenSslEngine(ByteBufAllocator alloc, String peerHost, int peerPort) throws Exception { private SSLEngine loadOpenSslEngine(ByteBufAllocator alloc, String peerHost, int peerPort) throws Exception {
final SslContext context; final SslContext context;
try { try {
if (kerb5Config == null && keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider)) checkSSLConfiguration();
throw new IllegalArgumentException("If \"" + TransportConstants.SSL_ENABLED_PROP_NAME + "\" is true then \"" + TransportConstants.KEYSTORE_PATH_PROP_NAME + "\" must be non-null " + "unless an alternative \"" + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME + "\" has been specified.");
context = new SSLSupport() context = new SSLSupport()
.setKeystoreProvider(keyStoreProvider) .setKeystoreProvider(keyStoreProvider)
.setKeystorePath(keyStorePath) .setKeystorePath(keyStorePath)
@ -647,8 +649,7 @@ public class NettyAcceptor extends AbstractAcceptor {
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin) .setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createNettyContext(); .createNettyContext();
} catch (Exception e) { } catch (Exception e) {
IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port); IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port, e);
ise.initCause(e);
throw ise; throw ise;
} }
Subject subject = null; Subject subject = null;

View File

@ -70,6 +70,7 @@ import org.apache.activemq.artemis.spi.core.remoting.AcceptorFactory;
import org.apache.activemq.artemis.spi.core.remoting.BufferHandler; import org.apache.activemq.artemis.spi.core.remoting.BufferHandler;
import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener; import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener;
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider;
import org.apache.activemq.artemis.utils.ActiveMQThreadFactory; import org.apache.activemq.artemis.utils.ActiveMQThreadFactory;
import org.apache.activemq.artemis.utils.ConfigurationHelper; import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.ReusableLatch; import org.apache.activemq.artemis.utils.ReusableLatch;
@ -380,6 +381,7 @@ public class RemotingServiceImpl implements RemotingService, ServerConnectionLif
if (!started) { if (!started) {
return; return;
} }
SSLContextFactoryProvider.getSSLContextFactory().clearSSLContexts();
failureCheckAndFlushThread.close(criticalError); failureCheckAndFlushThread.close(criticalError);

View File

@ -313,6 +313,12 @@ Please see the examples for a full working example of using Netty SSL.
Netty SSL uses all the same properties as Netty TCP but adds the following Netty SSL uses all the same properties as Netty TCP but adds the following
additional properties: additional properties:
- `sslContext`
A key that can be used in conjunction with `org.apache.activemq.artemis.core.remoting.impl.ssl.CachingSSLContextFactory`
to cache created SSLContext and avoid recreating. Look [Configuring a SSLContextFactory](#Configuring a SSLContextFactory)
for more details.
- `sslEnabled` - `sslEnabled`
Must be `true` to enable SSL. Default is `false`. Must be `true` to enable SSL. Default is `false`.
@ -490,6 +496,24 @@ additional properties:
[broker's classpath](using-server.md#adding-runtime-dependencies). [broker's classpath](using-server.md#adding-runtime-dependencies).
#### Configuring a SSLContextFactory
If you have a `JDK` provider you can configure which SSLContextFactory to use.
Currently we provide two implementations:
- `org.apache.activemq.artemis.core.remoting.impl.ssl.DefaultSSLContextFactory`
- `org.apache.activemq.artemis.core.remoting.impl.ssl.CachingSSLContextFactory`
but you can also add your own implementation of `org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory`.
The implementations are loaded by a ServiceLoader, thus you need to declare your implementation in
a `META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory` file.
If several implementations are available, the one with the highest `priority` will be selected.
So for example, if you want to use `org.apache.activemq.artemis.core.remoting.impl.ssl.CachingSSLContextFactory`
you need to add a `META-INF/services/org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory` file
to your classpath with the content `org.apache.activemq.artemis.core.remoting.impl.ssl.CachingSSLContextFactory`.
**Note:** This mechanism doesn't work if you have selected `OPENSSL` as provider.
### Configuring Netty HTTP ### Configuring Netty HTTP
Netty HTTP tunnels packets over the HTTP protocol. It can be useful in Netty HTTP tunnels packets over the HTTP protocol. It can be useful in