diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java index acaa72ad45..522986b719 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java @@ -36,6 +36,10 @@ public class TransportConstants { public static final String SSL_ENABLED_PROP_NAME = "sslEnabled"; + public static final String SSL_AUTO_RELOAD_PROP_NAME = "sslAutoReload"; + + public static final boolean DEFAULT_SSL_AUTO_RELOAD = false; + public static final String HTTP_ENABLED_PROP_NAME = "httpEnabled"; public static final String HTTP_CLIENT_IDLE_PROP_NAME = "httpClientIdleTime"; @@ -396,6 +400,7 @@ public class TransportConstants { static { Set allowableAcceptorKeys = new HashSet<>(); allowableAcceptorKeys.add(TransportConstants.SSL_ENABLED_PROP_NAME); + allowableAcceptorKeys.add(TransportConstants.SSL_AUTO_RELOAD_PROP_NAME); allowableAcceptorKeys.add(TransportConstants.HTTP_RESPONSE_TIME_PROP_NAME); allowableAcceptorKeys.add(TransportConstants.HTTP_SERVER_SCAN_PERIOD_PROP_NAME); allowableAcceptorKeys.add(TransportConstants.HTTP_UPGRADE_ENABLED_PROP_NAME); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java index 7f072eb1d4..80250512c4 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java @@ -23,6 +23,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.invoke.MethodHandles; import java.lang.management.ManagementFactory; +import java.net.MalformedURLException; import java.net.URL; import java.security.AccessController; import java.security.PrivilegedAction; @@ -63,6 +64,7 @@ import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.management.AcceptorControl; import org.apache.activemq.artemis.api.core.management.ResourceNames; import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryImpl; import org.apache.activemq.artemis.core.config.BridgeConfiguration; @@ -202,6 +204,7 @@ import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoader; import org.apache.activemq.artemis.utils.ActiveMQThreadFactory; import org.apache.activemq.artemis.utils.ActiveMQThreadPoolExecutor; import org.apache.activemq.artemis.utils.CompositeAddress; +import org.apache.activemq.artemis.utils.ConfigurationHelper; import org.apache.activemq.artemis.utils.ExecutorFactory; import org.apache.activemq.artemis.utils.ReusableLatch; import org.apache.activemq.artemis.utils.SecurityFormatter; @@ -220,6 +223,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.stream.Collectors.groupingBy; +import static org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.DEFAULT_SSL_AUTO_RELOAD; +import static org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.KEYSTORE_PATH_PROP_NAME; +import static org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.SSL_AUTO_RELOAD_PROP_NAME; +import static org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.TRUSTSTORE_PATH_PROP_NAME; import static org.apache.activemq.artemis.utils.collections.IterableStream.iterableOf; /** @@ -3361,6 +3368,25 @@ public class ActiveMQServerImpl implements ActiveMQServer { PropertiesLoader.reload(); }); } + + // track tls resources on acceptors and reload via remoting server + configuration.getAcceptorConfigurations().forEach((acceptorConfig) -> { + Map config = acceptorConfig.getCombinedParams(); + if (ConfigurationHelper.getBooleanProperty(SSL_AUTO_RELOAD_PROP_NAME, DEFAULT_SSL_AUTO_RELOAD, config)) { + URL pathUrl = fileUrlFrom(config.get(KEYSTORE_PATH_PROP_NAME)); + if (pathUrl != null) { + reloadManager.addCallback(pathUrl, (uri) -> { + reloadNamedAcceptor(acceptorConfig.getName()); + }); + } + pathUrl = fileUrlFrom(config.get(TRUSTSTORE_PATH_PROP_NAME)); + if (pathUrl != null) { + reloadManager.addCallback(pathUrl, (uri) -> { + reloadNamedAcceptor(acceptorConfig.getName()); + }); + } + } + }); } if (hasBrokerPlugins()) { @@ -3374,6 +3400,26 @@ public class ActiveMQServerImpl implements ActiveMQServer { return true; } + private void reloadNamedAcceptor(String name) { + // preference for Control to capture consistent audit logging + if (managementService != null) { + Object targetControl = managementService.getResource(ResourceNames.ACCEPTOR + name); + if (targetControl instanceof AcceptorControl) { + ((AcceptorControl) targetControl).reload(); + } + } + } + + private URL fileUrlFrom(Object o) { + if (o instanceof String) { + try { + return new File((String) o).toURI().toURL(); + } catch (MalformedURLException ignored) { + } + } + return null; + } + @Override public void installMirrorController(MirrorController mirrorController) { logger.debug("Mirror controller is being installed"); diff --git a/docs/user-manual/configuring-transports.adoc b/docs/user-manual/configuring-transports.adoc index fce760d233..4ad46ca63c 100644 --- a/docs/user-manual/configuring-transports.adoc +++ b/docs/user-manual/configuring-transports.adoc @@ -291,6 +291,11 @@ sslEnabled:: Must be `true` to enable SSL. Default is `false`. +sslAutoReload:: +Must be `true` to have the broker 'watch' an acceptors keyStorePath and/or trustStorePath and invoke reload() on update. +The watch period is controlled by xref:config-reload.adoc#configuration-reload[the configuration reload feature]. +Default is `false`. + keyStorePath:: When used on an `acceptor` this is the path to the SSL key store on the server which holds the server's certificates (whether self-signed or signed by an authority). + diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/SSLAutoReloadTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/SSLAutoReloadTest.java new file mode 100644 index 0000000000..79e919cfa7 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/SSLAutoReloadTest.java @@ -0,0 +1,85 @@ +/* + * 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.activemq.artemis.tests.integration.ssl; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.client.ActiveMQClient; +import org.apache.activemq.artemis.api.core.client.ServerLocator; +import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.apache.activemq.artemis.utils.Wait; +import org.junit.Test; + +/** + * See the tests/security-resources/build.sh script for details on the security resources used. + */ +public class SSLAutoReloadTest extends ActiveMQTestBase { + + private final String PASSWORD = "securepass"; + + @Test + public void testOneWaySSLWithAutoReload() throws Exception { + + File parentDir = new File(temporaryFolder.getRoot(), "sub"); + parentDir.mkdirs(); + + // reference keystore from temp location that we can update + final File keyStoreToReload = new File(parentDir, "server-ks.p12"); + copyRecursive(new File(this.getClass().getClassLoader().getResource("unknown-server-keystore.p12").getFile()), keyStoreToReload); + + Map params = new HashMap<>(); + params.put(TransportConstants.SSL_AUTO_RELOAD_PROP_NAME, true); + params.put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + params.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, keyStoreToReload.getAbsolutePath()); + params.put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, PASSWORD); + params.put(TransportConstants.HOST_PROP_NAME, "localhost"); + + ConfigurationImpl config = createBasicConfig().addAcceptorConfiguration(new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params, "nettySSL")); + ActiveMQServer server = createServer(false, config); + server.getConfiguration().setConfigurationFileRefreshPeriod(50); + server.start(); + waitForServerToStart(server); + + String url = "tcp://127.0.0.1:61616?sslEnabled=true;trustStorePath=server-ca-truststore.p12;trustStorePassword=" + PASSWORD; + ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocator(url)).setCallTimeout(3000); + + try { + createSessionFactory(locator); + fail("Creating session here should fail due to SSL handshake problems."); + } catch (Exception ignored) { + } + + // update the server side keystore + copyRecursive(new File(this.getClass().getClassLoader().getResource("server-keystore.p12").getFile()), keyStoreToReload); + + // expect success after auto reload, which we wait for + Wait.waitFor(() -> { + try { + addSessionFactory(createSessionFactory(locator)); + return true; + } catch (Throwable ignored) { + } + return false; + }, 5000, 100); + } +}