[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.concurrent.Future;
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.core.client.ActiveMQClientLogger;
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 org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider;
public class NettyConnector extends AbstractConnector {
public static String NIO_CONNECTOR_TYPE = "NIO";
@ -674,22 +676,10 @@ public class NettyConnector extends AbstractConnector {
String truststoreProvider,
String truststorePath,
String truststorePassword) throws Exception {
SSLContext context;
if (useDefaultSslContext) {
context = SSLContext.getDefault();
} else {
context = new SSLSupport()
.setKeystoreProvider(keystoreProvider)
.setKeystorePath(keystorePath)
.setKeystorePassword(keystorePassword)
.setTruststoreProvider(truststoreProvider)
.setTruststorePath(truststorePath)
.setTruststorePassword(truststorePassword)
.setTrustAll(trustAll)
.setCrlPath(crlPath)
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createContext();
}
SSLContext context = SSLContextFactoryProvider.getSSLContextFactory().getSSLContext(configuration,
keystoreProvider, keystorePath, keystorePassword,
truststoreProvider, truststorePath, truststorePassword,
crlPath, trustManagerFactoryPlugin, trustAll);
Subject subject = null;
if (kerb5Config != null) {
LoginContext loginContext = new LoginContext(kerb5Config);

View File

@ -29,6 +29,8 @@ public class TransportConstants {
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_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.Connection;
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.ConfigurationHelper;
import org.apache.activemq.artemis.utils.collections.TypedProperties;
@ -489,6 +490,7 @@ public class NettyAcceptor extends AbstractAcceptor {
// only for testing purposes
public void setKeyStorePath(String 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 {
SSLEngine engine;
if (sslProvider.equals(TransportConstants.OPENSSL_PROVIDER)) {
if (TransportConstants.OPENSSL_PROVIDER.equals(sslProvider)) {
engine = loadOpenSslEngine(alloc, peerHost, peerPort);
} else {
engine = loadJdkSslEngine(peerHost, peerPort);
@ -594,21 +596,13 @@ public class NettyAcceptor extends AbstractAcceptor {
private SSLEngine loadJdkSslEngine(String peerHost, int peerPort) throws Exception {
final SSLContext context;
try {
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.");
context = new SSLSupport()
.setKeystoreProvider(keyStoreProvider)
.setKeystorePath(keyStorePath)
.setKeystorePassword(keyStorePassword)
.setTruststoreProvider(trustStoreProvider)
.setTruststorePath(trustStorePath)
.setTruststorePassword(trustStorePassword)
.setCrlPath(crlPath)
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createContext();
checkSSLConfiguration();
context = SSLContextFactoryProvider.getSSLContextFactory().getSSLContext(configuration,
keyStoreProvider, keyStorePath, keyStorePassword,
trustStoreProvider, trustStorePath, trustStorePassword,
crlPath, trustManagerFactoryPlugin, TransportConstants.DEFAULT_TRUST_ALL);
} catch (Exception e) {
IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port);
ise.initCause(e);
IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port, e);
throw ise;
}
Subject subject = null;
@ -631,11 +625,19 @@ public class NettyAcceptor extends AbstractAcceptor {
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 {
final SslContext context;
try {
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.");
checkSSLConfiguration();
context = new SSLSupport()
.setKeystoreProvider(keyStoreProvider)
.setKeystorePath(keyStorePath)
@ -647,8 +649,7 @@ public class NettyAcceptor extends AbstractAcceptor {
.setTrustManagerFactoryPlugin(trustManagerFactoryPlugin)
.createNettyContext();
} catch (Exception e) {
IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port);
ise.initCause(e);
IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port, e);
throw ise;
}
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.Connection;
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.ConfigurationHelper;
import org.apache.activemq.artemis.utils.ReusableLatch;
@ -380,6 +381,7 @@ public class RemotingServiceImpl implements RemotingService, ServerConnectionLif
if (!started) {
return;
}
SSLContextFactoryProvider.getSSLContextFactory().clearSSLContexts();
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
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`
Must be `true` to enable SSL. Default is `false`.
@ -490,6 +496,24 @@ additional properties:
[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
Netty HTTP tunnels packets over the HTTP protocol. It can be useful in