From cd40ff2fdb4241dd24653d2fd44a1f05be9593d0 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 7 Jul 2026 20:55:16 +0300 Subject: [PATCH] IGNITE-28867 Implement TLS certificate hot redeployment via control.sh --ssl reload Reload TLS certificates on running nodes without a restart. The new `control.sh --ssl reload` command re-reads the configured key and trust stores from disk and replaces the active SSL context for the communication, discovery, thin client (client connector) and binary (TCP) REST transports. New connections use the updated certificates while established sessions are not interrupted. Co-Authored-By: Claude Opus 4.8 --- docs/_docs/tools/control-script.adoc | 21 ++ .../ignite/ssl/AbstractSslContextFactory.java | 32 +++ .../management/IgniteCommandRegistry.java | 4 +- .../internal/management/ssl/SslCommand.java | 30 +++ .../management/ssl/SslReloadCommand.java | 60 +++++ .../management/ssl/SslReloadTask.java | 102 +++++++++ .../odbc/ClientListenerProcessor.java | 36 ++- .../processors/rest/GridRestProcessor.java | 13 +- .../processors/rest/GridRestProtocol.java | 11 + .../protocols/tcp/GridTcpRestProtocol.java | 33 ++- .../util/nio/ssl/GridNioSslFilter.java | 14 +- .../util/nio/ssl/SslContextReloadable.java | 38 ++++ .../tcp/TcpCommunicationSpi.java | 10 +- .../tcp/internal/GridNioServerWrapper.java | 30 +++ .../spi/discovery/tcp/TcpDiscoverySpi.java | 37 ++- .../ssl/SslContextFactoryReloadTest.java | 214 ++++++++++++++++++ .../ignite/ssl/SslContextReloadNodeTest.java | 160 +++++++++++++ .../ignite/testsuites/SecurityTestSuite.java | 4 + ...mmandHandlerClusterByClassTest_help.output | 3 + ...ndlerClusterByClassWithSSLTest_help.output | 3 + 20 files changed, 840 insertions(+), 15 deletions(-) create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslCommand.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadCommand.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadTask.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/SslContextReloadable.java create mode 100644 modules/core/src/test/java/org/apache/ignite/ssl/SslContextFactoryReloadTest.java create mode 100644 modules/core/src/test/java/org/apache/ignite/ssl/SslContextReloadNodeTest.java diff --git a/docs/_docs/tools/control-script.adoc b/docs/_docs/tools/control-script.adoc index 3329f1c415633..45b03e8e02749 100644 --- a/docs/_docs/tools/control-script.adoc +++ b/docs/_docs/tools/control-script.adoc @@ -1688,3 +1688,24 @@ Examples: ---- control.sh|bat --event list --enabled ---- + +== TLS Certificate Hot Reload + +By default, TLS certificates are loaded once at node startup, so updating them requires a full node restart. +The `--ssl reload` command reloads certificates at runtime on all server nodes without a restart: each node +re-reads the key and trust stores from disk using its configured `SslContextFactory` and replaces the active +SSL context. New connections use the updated certificates, while sessions that were already established are not +interrupted. + +[source, shell] +---- +control.sh|bat --ssl reload +---- + +The command applies to the communication, discovery, thin client (client connector) and binary (TCP) REST transports +that have SSL enabled. Update the certificate files on disk (keeping the configured key and trust store paths) before +running the command. + +NOTE: For the discovery transport, new outgoing connections use the reloaded certificates immediately, but the +already-bound listening socket keeps serving the previously loaded certificate to incoming connections until it is +recreated (for example, when the node re-enters the ring). diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 77dedf84eba78..36a0f4dcf235f 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -196,4 +196,36 @@ protected void checkNullParameter(Object param, String name) throws SSLException return ctx; } + + /** + * Rebuilds the SSL context from the current factory configuration, re-reading the configured key and trust + * stores from disk, and replaces the instance cached by {@link #create()}. Used to hot-reload TLS certificates + * at runtime without a node restart. + * + * @return Newly created SSL context. + * @throws SSLException If the new context could not be created. In this case the previously cached context is + * kept intact. + */ + public SSLContext reload() throws SSLException { + SSLContext ctx = createSslContext(); + + sslCtx.set(ctx); + + return ctx; + } + + /** + * Rebuilds the SSL context produced by the given factory. If the factory supports hot reload (is an + * {@link AbstractSslContextFactory}) its cached context is rebuilt from the configured key and trust stores; + * otherwise {@link Factory#create()} is invoked. + * + * @param factory SSL context factory. + * @return Reloaded SSL context. + * @throws SSLException If the context could not be reloaded. + */ + public static SSLContext reload(Factory factory) throws SSLException { + return factory instanceof AbstractSslContextFactory + ? ((AbstractSslContextFactory)factory).reload() + : factory.create(); + } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java b/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java index f7c9ea9c57fcb..130ac72149431 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java @@ -37,6 +37,7 @@ import org.apache.ignite.internal.management.persistence.PersistenceCommand; import org.apache.ignite.internal.management.property.PropertyCommand; import org.apache.ignite.internal.management.snapshot.SnapshotCommand; +import org.apache.ignite.internal.management.ssl.SslCommand; import org.apache.ignite.internal.management.tracing.TracingConfigurationCommand; import org.apache.ignite.internal.management.tx.TxCommand; import org.apache.ignite.internal.management.wal.WalCommand; @@ -77,7 +78,8 @@ public IgniteCommandRegistry() { new PerformanceStatisticsCommand(), new CdcCommand(), new ConsistencyCommand(), - new EventCommand() + new EventCommand(), + new SslCommand() ); U.loadService(CommandsProvider.class).forEach(p -> p.commands().forEach(this::register)); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslCommand.java new file mode 100644 index 0000000000000..ce6422eee07c3 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslCommand.java @@ -0,0 +1,30 @@ +/* + * 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.ignite.internal.management.ssl; + +import org.apache.ignite.internal.management.api.CommandRegistryImpl; + +/** SSL features command. */ +public class SslCommand extends CommandRegistryImpl { + /** */ + public SslCommand() { + super( + new SslReloadCommand() + ); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadCommand.java new file mode 100644 index 0000000000000..56df7adf2e846 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadCommand.java @@ -0,0 +1,60 @@ +/* + * 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.ignite.internal.management.ssl; + +import java.util.Collection; +import java.util.function.Consumer; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.internal.management.api.CommandUtils; +import org.apache.ignite.internal.management.api.ComputeCommand; +import org.apache.ignite.internal.management.api.NoArg; +import org.jetbrains.annotations.Nullable; + +/** Reloads TLS certificates on all server nodes without a restart. */ +public class SslReloadCommand implements ComputeCommand { + /** {@inheritDoc} */ + @Override public String description() { + return "Reload TLS certificates on all server nodes by re-reading the configured key and trust stores. " + + "New connections use the updated certificates while established sessions are not interrupted"; + } + + /** {@inheritDoc} */ + @Override public Class argClass() { + return NoArg.class; + } + + /** {@inheritDoc} */ + @Override public Class taskClass() { + return SslReloadTask.class; + } + + /** {@inheritDoc} */ + @Override public @Nullable Collection nodes(Collection nodes, NoArg arg) { + return CommandUtils.servers(nodes); + } + + /** {@inheritDoc} */ + @Override public void printResult(NoArg arg, String res, Consumer printer) { + printer.accept(res); + } + + /** {@inheritDoc} */ + @Override public String confirmationPrompt(NoArg arg) { + return "Warning: the command will reload TLS certificates on all server nodes."; + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadTask.java b/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadTask.java new file mode 100644 index 0000000000000..efb6a43358b0c --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/ssl/SslReloadTask.java @@ -0,0 +1,102 @@ +/* + * 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.ignite.internal.management.ssl; + +import java.util.ArrayList; +import java.util.List; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteException; +import org.apache.ignite.compute.ComputeJobResult; +import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.management.api.NoArg; +import org.apache.ignite.internal.processors.task.GridInternal; +import org.apache.ignite.internal.util.nio.ssl.SslContextReloadable; +import org.apache.ignite.internal.visor.VisorJob; +import org.apache.ignite.internal.visor.VisorMultiNodeTask; +import org.jetbrains.annotations.Nullable; + +/** Task that reloads TLS certificates on every mapped node. */ +@GridInternal +public class SslReloadTask extends VisorMultiNodeTask { + /** */ + private static final long serialVersionUID = 0L; + + /** {@inheritDoc} */ + @Override protected VisorJob job(NoArg arg) { + return new SslReloadJob(arg, debug); + } + + /** {@inheritDoc} */ + @Override protected @Nullable String reduce0(List results) throws IgniteException { + StringBuilder res = new StringBuilder(); + + for (ComputeJobResult jobRes : results) { + if (jobRes.getException() != null) + throw jobRes.getException(); + + res.append(jobRes.getData().toString()).append('\n'); + } + + return res.toString(); + } + + /** Job that reloads TLS certificates on the local node. */ + private static class SslReloadJob extends VisorJob { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + protected SslReloadJob(NoArg arg, boolean debug) { + super(arg, debug); + } + + /** {@inheritDoc} */ + @Override protected String run(NoArg arg) throws IgniteException { + GridKernalContext ctx = ignite.context(); + + List reloaded = new ArrayList<>(); + + try { + reload(ctx.config().getCommunicationSpi(), "communication", reloaded); + reload(ctx.config().getDiscoverySpi(), "discovery", reloaded); + reload(ctx.clientListener(), "client connector", reloaded); + reload(ctx.rest(), "REST", reloaded); + } + catch (IgniteCheckedException e) { + throw new IgniteException("Failed to reload SSL certificates on node " + + ignite.localNode().id() + ": " + e.getMessage(), e); + } + + return ignite.localNode().id() + ": " + + (reloaded.isEmpty() ? "SSL is not configured, nothing to reload" : "reloaded " + reloaded); + } + + /** + * Reloads the SSL context of the given component if it supports hot reload. + * + * @param comp Component to reload. + * @param name Human-readable component name for the result message. + * @param out Collects names of the components that were actually reloaded. + * @throws IgniteCheckedException If the reload failed. + */ + private static void reload(Object comp, String name, List out) throws IgniteCheckedException { + if (comp instanceof SslContextReloadable && ((SslContextReloadable)comp).reloadSslContext()) + out.add(name); + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java index 68af9dfe14aeb..742cb86587c88 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerProcessor.java @@ -34,6 +34,7 @@ import javax.management.JMException; import javax.management.ObjectName; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.configuration.ClientConnectorConfiguration; @@ -57,6 +58,7 @@ import org.apache.ignite.internal.util.nio.GridNioServer; import org.apache.ignite.internal.util.nio.GridNioSession; import org.apache.ignite.internal.util.nio.ssl.GridNioSslFilter; +import org.apache.ignite.internal.util.nio.ssl.SslContextReloadable; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.internal.util.worker.GridWorker; @@ -68,6 +70,7 @@ import org.apache.ignite.spi.IgnitePortProtocol; import org.apache.ignite.spi.systemview.view.ClientConnectionAttributeView; import org.apache.ignite.spi.systemview.view.ClientConnectionView; +import org.apache.ignite.ssl.AbstractSslContextFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -84,7 +87,7 @@ /** * Client connector processor. */ -public class ClientListenerProcessor extends GridProcessorAdapter { +public class ClientListenerProcessor extends GridProcessorAdapter implements SslContextReloadable { /** */ public static final String CLI_CONN_VIEW = metricName("client", "connections"); @@ -118,6 +121,9 @@ public class ClientListenerProcessor extends GridProcessorAdapter { /** TCP Server. */ private GridNioServer srv; + /** SSL filter of the current server, {@code null} if SSL is disabled. Used to hot-reload certificates. */ + private volatile GridNioSslFilter sslFilter; + /** Metrics. */ private ClientListenerMetrics metrics; @@ -502,6 +508,8 @@ else if (connCtx.managementClient()) { sslFilter.wantClientAuth(auth); sslFilter.needClientAuth(auth); + this.sslFilter = sslFilter; + return new GridNioFilter[] { openSesFilter, codecFilter, @@ -516,6 +524,32 @@ else if (connCtx.managementClient()) { } } + /** {@inheritDoc} */ + @Override public boolean reloadSslContext() throws IgniteCheckedException { + ClientConnectorConfiguration cliConnCfg = ctx.config().getClientConnectorConfiguration(); + + GridNioSslFilter filter = sslFilter; + + if (cliConnCfg == null || !cliConnCfg.isSslEnabled() || filter == null) + return false; + + Factory sslCtxFactory = cliConnCfg.isUseIgniteSslContextFactory() + ? ctx.config().getSslContextFactory() + : cliConnCfg.getSslContextFactory(); + + if (sslCtxFactory == null) + return false; + + try { + filter.updateSslContext(AbstractSslContextFactory.reload(sslCtxFactory)); + } + catch (SSLException e) { + throw new IgniteCheckedException("Failed to reload SSL context for client connector connections.", e); + } + + return true; + } + /** {@inheritDoc} */ @Override public void onKernalStop(boolean cancel) { if (srv != null) { diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java index a7e0cc58d9d27..1f632724c50f7 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProcessor.java @@ -72,6 +72,7 @@ import org.apache.ignite.internal.thread.context.Scope; import org.apache.ignite.internal.util.GridSpinReadWriteLock; import org.apache.ignite.internal.util.future.GridFinishedFuture; +import org.apache.ignite.internal.util.nio.ssl.SslContextReloadable; import org.apache.ignite.internal.util.typedef.C1; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.X; @@ -106,7 +107,7 @@ /** * Rest processor implementation. */ -public class GridRestProcessor extends GridProcessorAdapter implements IgniteRestProcessor { +public class GridRestProcessor extends GridProcessorAdapter implements IgniteRestProcessor, SslContextReloadable { /** HTTP protocol class name. */ private static final String HTTP_PROTO_CLS = "org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyRestProtocol"; @@ -609,6 +610,16 @@ private boolean notStartOnClient() { } } + /** {@inheritDoc} */ + @Override public boolean reloadSslContext() throws IgniteCheckedException { + boolean reloaded = false; + + for (GridRestProtocol proto : protos) + reloaded |= proto.reloadSslContext(); + + return reloaded; + } + /** {@inheritDoc} */ @SuppressWarnings("BusyWait") @Override public void onKernalStop(boolean cancel) { diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProtocol.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProtocol.java index 073ce0c491e82..62f41f1a070e5 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProtocol.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/GridRestProtocol.java @@ -60,4 +60,15 @@ public interface GridRestProtocol { * Processor start callback. */ void onProcessorStart(); + + /** + * Reloads the SSL context used by this protocol so that new connections use the updated certificates. Default + * implementation is a no-op for protocols that do not support hot reload. + * + * @return {@code true} if SSL is enabled and the context was reloaded, {@code false} otherwise. + * @throws IgniteCheckedException If the SSL context could not be reloaded. + */ + public default boolean reloadSslContext() throws IgniteCheckedException { + return false; + } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/protocols/tcp/GridTcpRestProtocol.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/protocols/tcp/GridTcpRestProtocol.java index a1aaa0910dd55..211f67eae3001 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/protocols/tcp/GridTcpRestProtocol.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/rest/protocols/tcp/GridTcpRestProtocol.java @@ -50,6 +50,7 @@ import org.apache.ignite.marshaller.MarshallerUtils; import org.apache.ignite.plugin.PluginProvider; import org.apache.ignite.spi.IgnitePortProtocol; +import org.apache.ignite.ssl.AbstractSslContextFactory; import org.jetbrains.annotations.Nullable; import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName; @@ -61,6 +62,12 @@ public class GridTcpRestProtocol extends GridRestProtocolAdapter { /** Server. */ private GridNioServer srv; + /** SSL filter of the current server, {@code null} if SSL is disabled. Used to hot-reload certificates. */ + private volatile GridNioSslFilter sslFilter; + + /** SSL context factory used to (re)create the SSL context, {@code null} if SSL is disabled. */ + private volatile Factory sslCtxFactory; + /** NIO server listener. */ private GridTcpRestNioListener lsnr; @@ -103,10 +110,9 @@ public GridTcpRestProtocol(GridKernalContext ctx) { // Thrown SSL exception instead of IgniteCheckedException for writing correct warning message into log. throw new SSLException("SSL is enabled, but SSL context factory is not specified."); - if (factory != null) - sslCtx = factory.create(); - else - sslCtx = igniteFactory.create(); + sslCtxFactory = factory != null ? factory : igniteFactory; + + sslCtx = sslCtxFactory.create(); } int startPort = cfg.getPort(); int portRange = cfg.getPortRange(); @@ -173,6 +179,23 @@ public GridTcpRestProtocol(GridKernalContext ctx) { log.info(stopInfo()); } + /** {@inheritDoc} */ + @Override public boolean reloadSslContext() throws IgniteCheckedException { + GridNioSslFilter filter = sslFilter; + + if (filter == null || sslCtxFactory == null) + return false; + + try { + filter.updateSslContext(AbstractSslContextFactory.reload(sslCtxFactory)); + } + catch (SSLException e) { + throw new IgniteCheckedException("Failed to reload SSL context for REST connections.", e); + } + + return true; + } + /** * Resolves host for REST TCP server using grid configuration. * @@ -224,6 +247,8 @@ private boolean startTcpServer(InetAddress hostAddr, int port, GridNioServerList sslFilter.needClientAuth(auth); + this.sslFilter = sslFilter; + filters = new GridNioFilter[] { codec, sslFilter diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java b/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java index ab9f29946999b..bfee6a4bb4a34 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java @@ -70,8 +70,8 @@ public class GridNioSslFilter extends GridNioFilterAdapter { /** Array of enabled protocols. */ private String[] enabledProtos; - /** SSL context to use. */ - private SSLContext sslCtx; + /** SSL context to use. Volatile to support hot reload of certificates via {@link #updateSslContext(SSLContext)}. */ + private volatile SSLContext sslCtx; /** Order. */ private ByteOrder order; @@ -126,6 +126,16 @@ public GridNioSslFilter( ); } + /** + * Replaces the SSL context used to create an {@link SSLEngine} for new sessions. Sessions that were already + * opened keep using their existing engines and are not affected. Used to hot-reload TLS certificates. + * + * @param sslCtx New SSL context. + */ + public void updateSslContext(SSLContext sslCtx) { + this.sslCtx = sslCtx; + } + /** * @param directMode Flag indicating whether direct mode is used. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/SslContextReloadable.java b/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/SslContextReloadable.java new file mode 100644 index 0000000000000..5616bc674e092 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/util/nio/ssl/SslContextReloadable.java @@ -0,0 +1,38 @@ +/* + * 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.ignite.internal.util.nio.ssl; + +import org.apache.ignite.IgniteCheckedException; + +/** + * A component whose TLS/SSL context (certificates) can be hot-reloaded at runtime without a node restart. New + * connections established after the reload use the updated certificates, while sessions that were already + * established keep using their existing engines and are not interrupted. + */ +public interface SslContextReloadable { + /** + * Rebuilds the SSL context from the configured factory (re-reading the key and trust stores from disk) and + * applies it so that new connections use the updated certificates. + * + * @return {@code true} if SSL is enabled for this component and the context was reloaded; {@code false} if SSL + * is not configured (no-op). + * @throws IgniteCheckedException If the SSL context could not be reloaded (for example, the new key store is + * invalid). The previously active context is kept in this case. + */ + public boolean reloadSslContext() throws IgniteCheckedException; +} diff --git a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationSpi.java b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationSpi.java index f9ab18727eada..e47cd8886a7c9 100755 --- a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationSpi.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationSpi.java @@ -54,6 +54,7 @@ import org.apache.ignite.internal.util.nio.GridNioServer; import org.apache.ignite.internal.util.nio.GridNioSession; import org.apache.ignite.internal.util.nio.GridNioSessionMetaKey; +import org.apache.ignite.internal.util.nio.ssl.SslContextReloadable; import org.apache.ignite.internal.util.tostring.GridToStringInclude; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.internal.worker.WorkersRegistry; @@ -195,7 +196,7 @@ @IgniteSpiMultipleInstancesSupport(true) @IgniteSpiConsistencyChecked(optional = false) @GridToStringInclude -public class TcpCommunicationSpi extends TcpCommunicationConfigInitializer { +public class TcpCommunicationSpi extends TcpCommunicationConfigInitializer implements SslContextReloadable { /** Node attribute that is mapped to node IP addresses (value is comm.tcp.addrs). */ public static final String ATTR_ADDRS = "comm.tcp.addrs"; @@ -421,6 +422,13 @@ public static boolean isCommunicationMetrics(String metricName) { metricsLsnr.resetMetrics(); } + /** {@inheritDoc} */ + @Override public boolean reloadSslContext() throws IgniteCheckedException { + GridNioServerWrapper wrapper = nioSrvWrapper; + + return wrapper != null && wrapper.reloadSslContext(); + } + /** * @param nodeId Target node ID. * @return Future. diff --git a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java index df7935c6c2f41..3f3f4e7868d9b 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java @@ -102,6 +102,7 @@ import org.apache.ignite.spi.communication.tcp.messages.NodeIdMessage; import org.apache.ignite.spi.communication.tcp.messages.RecoveryLastReceivedMessage; import org.apache.ignite.spi.discovery.IgniteDiscoveryThread; +import org.apache.ignite.ssl.AbstractSslContextFactory; import org.jetbrains.annotations.Nullable; import static org.apache.ignite.internal.thread.pool.IgniteScheduledThreadPoolExecutor.newSingleThreadScheduledExecutor; @@ -218,6 +219,9 @@ public class GridNioServerWrapper { /** NIO server. */ private GridNioServer nioSrv; + /** SSL filter of the current NIO server, {@code null} if SSL is disabled. Used to hot-reload certificates. */ + private volatile GridNioSslFilter sslFilter; + /** Stopping flag (set to {@code true} when SPI gets stopping signal). */ private volatile boolean stopping = false; @@ -721,6 +725,30 @@ public void nio(GridNioServer srv) { nioSrv = srv; } + /** + * Reloads the SSL context used for communication connections, re-reading the certificates from the configured + * SSL context factory. New inbound connections use the updated certificates via the NIO SSL filter; new outbound + * connections pick up the reloaded context from the factory automatically. Established sessions are not affected. + * + * @return {@code true} if SSL is enabled and the context was reloaded, {@code false} otherwise. + * @throws IgniteCheckedException If the SSL context could not be reloaded. + */ + public boolean reloadSslContext() throws IgniteCheckedException { + GridNioSslFilter filter = sslFilter; + + if (!stateProvider.isSslEnabled() || filter == null) + return false; + + try { + filter.updateSslContext(AbstractSslContextFactory.reload(igniteCfg.getSslContextFactory())); + } + catch (SSLException e) { + throw new IgniteCheckedException("Failed to reload SSL context for communication connections.", e); + } + + return true; + } + /** * @param node Node. * @param key Connection key. @@ -922,6 +950,8 @@ private MessageFactory get() { sslFilter.needClientAuth(true); filters.add(sslFilter); + + this.sslFilter = sslFilter; } GridNioServer.Builder builder = GridNioServer.builder() diff --git a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java index eb868103ad1c2..c02d14fca0d6e 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java @@ -60,6 +60,7 @@ import org.apache.ignite.internal.managers.discovery.IgniteDiscoverySpi; import org.apache.ignite.internal.processors.failure.FailureProcessor; import org.apache.ignite.internal.processors.metric.MetricRegistryImpl; +import org.apache.ignite.internal.util.nio.ssl.SslContextReloadable; import org.apache.ignite.internal.util.tostring.GridToStringExclude; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.X; @@ -109,6 +110,7 @@ import org.apache.ignite.spi.discovery.tcp.messages.TcpDiscoveryDuplicateIdMessage; import org.apache.ignite.spi.discovery.tcp.messages.TcpDiscoveryEnsureDelivery; import org.apache.ignite.spi.discovery.tcp.messages.TcpDiscoveryHandshakeResponse; +import org.apache.ignite.ssl.AbstractSslContextFactory; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -229,7 +231,7 @@ @DiscoverySpiOrderSupport(true) @DiscoverySpiHistorySupport(true) @DiscoverySpiMutableCustomMessageSupport(true) -public class TcpDiscoverySpi extends IgniteSpiAdapter implements IgniteDiscoverySpi { +public class TcpDiscoverySpi extends IgniteSpiAdapter implements IgniteDiscoverySpi, SslContextReloadable { /** Node attribute that is mapped to node's external addresses (value is disc.tcp.ext-addrs). */ public static final String ATTR_EXT_ADDRS = "disc.tcp.ext-addrs"; @@ -419,11 +421,11 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter implements IgniteDiscovery /** Node authenticator. */ protected DiscoverySpiNodeAuthenticator nodeAuth; - /** SSL server socket factory. */ - protected SSLServerSocketFactory sslSrvSockFactory; + /** SSL server socket factory. Volatile since it may be replaced at runtime by {@link #reloadSslContext()}. */ + protected volatile SSLServerSocketFactory sslSrvSockFactory; - /** SSL socket factory. */ - protected SSLSocketFactory sslSockFactory; + /** SSL socket factory. Volatile since it may be replaced at runtime by {@link #reloadSslContext()}. */ + protected volatile SSLSocketFactory sslSockFactory; /** SSL enable/disable flag. */ protected boolean sslEnable; @@ -2308,6 +2310,31 @@ boolean isSslEnabled() { return sslEnable; } + /** + * {@inheritDoc} + *

+ * New outgoing discovery connections use the reloaded certificates immediately. The already-bound listening + * server socket keeps serving the previously loaded certificate to incoming connections until it is recreated + * (for example, on this node re-entering the ring), because {@link javax.net.ssl.SSLServerSocket} captures the + * SSL context at creation time. + */ + @Override public boolean reloadSslContext() throws IgniteCheckedException { + if (!sslEnable) + return false; + + try { + SSLContext sslCtx = AbstractSslContextFactory.reload(ignite.configuration().getSslContextFactory()); + + sslSockFactory = sslCtx.getSocketFactory(); + sslSrvSockFactory = sslCtx.getServerSocketFactory(); + } + catch (SSLException e) { + throw new IgniteCheckedException("Failed to reload SSL context for discovery connections.", e); + } + + return true; + } + /** {@inheritDoc} */ @Override public void clientReconnect() throws IgniteSpiException { impl.reconnect(); diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextFactoryReloadTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextFactoryReloadTest.java new file mode 100644 index 0000000000000..14b022f98f8f8 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextFactoryReloadTest.java @@ -0,0 +1,214 @@ +/* + * 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.ignite.ssl; + +import java.io.OutputStream; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +/** + * Tests hot reload of an {@link SslContextFactory}: {@link AbstractSslContextFactory#reload()} must re-read the + * key store from disk and start serving the updated certificate, while keeping the cache semantics of + * {@link AbstractSslContextFactory#create()}. + */ +public class SslContextFactoryReloadTest extends GridCommonAbstractTest { + /** Executor for the accepting side of the loopback handshake. */ + private ExecutorService exec; + + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + exec = Executors.newSingleThreadExecutor(); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + if (exec != null) + exec.shutdownNow(); + } + + /** + * Points a factory at a key store file, then overwrites that file with a different key store and reloads the + * factory. The certificate served over a real TLS handshake must change, while {@code create()} keeps returning + * the currently active (cached) context. + */ + @Test + public void testReloadServesNewCertificate() throws Exception { + Path keyStore = Files.createTempFile("advui-reload-", ".jks"); + + try { + copyKeyStore("node01", keyStore); + + SslContextFactory factory = factory(keyStore); + + SSLContext ctx1 = factory.create(); + + // create() must cache the context. + assertSame(ctx1, factory.create()); + + X509Certificate cert1 = serverCertificate(ctx1); + + copyKeyStore("node02", keyStore); + + SSLContext ctx2 = factory.reload(); + + assertNotSame("reload() must build a new context", ctx1, ctx2); + + // create() must return the reloaded context. + assertSame(ctx2, factory.create()); + + X509Certificate cert2 = serverCertificate(ctx2); + + assertFalse( + "Reloaded context must serve a different certificate", + Arrays.equals(cert1.getEncoded(), cert2.getEncoded())); + } + finally { + Files.deleteIfExists(keyStore); + } + } + + /** + * {@link AbstractSslContextFactory#reload(javax.cache.configuration.Factory)} must reload our own factories and + * return a freshly built context. + */ + @Test + public void testStaticReloadHelper() throws Exception { + Path keyStore = Files.createTempFile("advui-reload-", ".jks"); + + try { + copyKeyStore("node01", keyStore); + + SslContextFactory factory = factory(keyStore); + + SSLContext ctx1 = factory.create(); + + SSLContext ctx2 = AbstractSslContextFactory.reload(factory); + + assertNotSame(ctx1, ctx2); + assertSame(ctx2, factory.create()); + } + finally { + Files.deleteIfExists(keyStore); + } + } + + /** + * @param keyStore Key store file the factory reads its certificate from. + * @return Factory with a disabled trust manager (only the key side is exercised in this test). + */ + private SslContextFactory factory(Path keyStore) { + SslContextFactory factory = new SslContextFactory(); + + factory.setKeyStoreFilePath(keyStore.toString()); + factory.setKeyStorePassword(GridTestUtils.keyStorePassword().toCharArray()); + factory.setTrustManagers(SslContextFactory.getDisabledTrustManager()); + + return factory; + } + + /** + * @param name Test key store name (see {@code tests.properties}). + * @param dest Destination file. + */ + private void copyKeyStore(String name, Path dest) throws Exception { + Files.copy(Path.of(GridTestUtils.keyStorePath(name)), dest, StandardCopyOption.REPLACE_EXISTING); + } + + /** + * Performs a loopback TLS handshake using the given context on the server side and a trust-all context on the + * client side, and returns the certificate the server presented. + * + * @param srvCtx Server SSL context under test. + * @return Server certificate seen by the client. + */ + private X509Certificate serverCertificate(SSLContext srvCtx) throws Exception { + InetAddress loopback = InetAddress.getLoopbackAddress(); + + try (SSLServerSocket srvSock = (SSLServerSocket)srvCtx.getServerSocketFactory() + .createServerSocket(0, 0, loopback)) { + + Future accepted = exec.submit((Callable)() -> { + try (SSLSocket s = (SSLSocket)srvSock.accept()) { + s.startHandshake(); + + // Read one byte to let the client finish the handshake and drive the exchange. + s.getInputStream().read(); + } + + return null; + }); + + try (SSLSocket cli = (SSLSocket)trustAllContext().getSocketFactory() + .createSocket(loopback, srvSock.getLocalPort())) { + + cli.startHandshake(); + + X509Certificate cert = (X509Certificate)cli.getSession().getPeerCertificates()[0]; + + OutputStream out = cli.getOutputStream(); + out.write(1); + out.flush(); + + accepted.get(); + + return cert; + } + } + } + + /** + * @return Client-side context that trusts any server certificate. + */ + private SSLContext trustAllContext() throws Exception { + TrustManager trustAll = new X509TrustManager() { + @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { + // No-op. + } + + @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { + // No-op. + } + + @Override public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + SSLContext ctx = SSLContext.getInstance("TLS"); + + ctx.init(null, new TrustManager[] {trustAll}, null); + + return ctx; + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextReloadNodeTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextReloadNodeTest.java new file mode 100644 index 0000000000000..0c0aa67d098ea --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextReloadNodeTest.java @@ -0,0 +1,160 @@ +/* + * 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.ignite.ssl; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.UUID; +import javax.cache.configuration.Factory; +import javax.net.ssl.SSLContext; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.configuration.ClientConnectorConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.management.api.NoArg; +import org.apache.ignite.internal.management.ssl.SslReloadTask; +import org.apache.ignite.internal.visor.VisorTaskArgument; +import org.apache.ignite.internal.visor.VisorTaskResult; +import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +/** + * Tests hot reload of TLS certificates on a running node via the {@code --ssl reload} command task: the SSL-enabled + * transports must reload after the key store on disk has been replaced, while the cluster keeps operating + * (established sessions are not interrupted). + */ +public class SslContextReloadNodeTest extends GridCommonAbstractTest { + /** Key store file shared by the nodes; replaced on disk to simulate certificate rotation. */ + private Path keyStore; + + /** Whether SSL should be configured for the node being started. */ + private boolean ssl = true; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + if (ssl) { + cfg.setSslContextFactory(reloadableFactory()); + + cfg.setClientConnectorConfiguration(new ClientConnectorConfiguration() + .setSslEnabled(true) + .setSslClientAuth(false) + .setUseIgniteSslContextFactory(true)); + } + + return cfg; + } + + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + keyStore = Files.createTempFile("advui-node-reload-", ".jks"); + + copyKeyStore("node01"); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + + if (keyStore != null) + Files.deleteIfExists(keyStore); + } + + /** Certificate reload on every SSL transport of a running two-node cluster must succeed and keep it operational. */ + @Test + public void testReloadOnRunningCluster() throws Exception { + IgniteEx g0 = startGrid(0); + IgniteEx g1 = startGrid(1); + + IgniteCache cache = g0.getOrCreateCache(DEFAULT_CACHE_NAME); + + cache.put(1, 1); + + assertEquals((Integer)1, g1.cache(DEFAULT_CACHE_NAME).get(1)); + + // Rotate the certificate on disk. + copyKeyStore("node02"); + + String res = reload(g0, g0.localNode().id(), g1.localNode().id()); + + assertTrue(res, res.contains("communication")); + assertTrue(res, res.contains("discovery")); + assertTrue(res, res.contains("client connector")); + + // Established sessions are not interrupted: the cluster keeps working after the reload. + cache.put(2, 2); + + assertEquals((Integer)2, g1.cache(DEFAULT_CACHE_NAME).get(2)); + } + + /** Reload must report nothing to reload on a node that does not use SSL. */ + @Test + public void testReloadWithoutSsl() throws Exception { + ssl = false; + + try { + IgniteEx g = startGrid(0); + + String res = reload(g, g.localNode().id()); + + assertTrue(res, res.contains("SSL is not configured")); + } + finally { + ssl = true; + } + } + + /** + * Executes the {@code --ssl reload} task on the given nodes. + * + * @param ignite Node to submit the task from. + * @param nodeIds Nodes to reload certificates on. + * @return Aggregated task result. + */ + private String reload(IgniteEx ignite, UUID... nodeIds) throws Exception { + VisorTaskResult res = ignite.compute().execute(SslReloadTask.class, + new VisorTaskArgument<>(Arrays.asList(nodeIds), new NoArg(), false)); + + return res.result(); + } + + /** + * @return SSL context factory that reads its certificate from {@link #keyStore} (so a reload picks up an + * updated file) and trusts any peer certificate. + */ + private Factory reloadableFactory() { + SslContextFactory factory = new SslContextFactory(); + + factory.setKeyStoreFilePath(keyStore.toString()); + factory.setKeyStorePassword(GridTestUtils.keyStorePassword().toCharArray()); + factory.setTrustManagers(SslContextFactory.getDisabledTrustManager()); + + return factory; + } + + /** + * @param name Test key store name (see {@code tests.properties}). + */ + private void copyKeyStore(String name) throws Exception { + Files.copy(Path.of(GridTestUtils.keyStorePath(name)), keyStore, StandardCopyOption.REPLACE_EXISTING); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java index ba4e875162cad..76f10928c070b 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java @@ -75,6 +75,8 @@ import org.apache.ignite.internal.processors.security.snapshot.SnapshotPermissionCheckTest; import org.apache.ignite.internal.thread.context.OperationContextAttributesTest; import org.apache.ignite.ssl.MultipleSSLContextsTest; +import org.apache.ignite.ssl.SslContextFactoryReloadTest; +import org.apache.ignite.ssl.SslContextReloadNodeTest; import org.apache.ignite.tools.junit.JUnitTeamcityReporter; import org.junit.BeforeClass; import org.junit.runner.RunWith; @@ -137,6 +139,8 @@ IgniteSecurityProcessorTest.class, MultipleSSLContextsTest.class, + SslContextFactoryReloadTest.class, + SslContextReloadNodeTest.class, MaintenanceModeNodeSecurityTest.class, ServiceAuthorizationTest.class, ServiceStaticConfigTest.class, diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output index 93f3954320266..259485bbdf891 100644 --- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output +++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output @@ -506,6 +506,9 @@ If the file name isn't specified the output file name is: '.bin': Parameters: --enabled - Filter only enabled events. + Reload TLS certificates on all server nodes by re-reading the configured key and trust stores. New connections use the updated certificates while established sessions are not interrupted: + control.(sh|bat) --ssl reload + By default commands affecting the cluster require interactive confirmation. Use --yes option to disable it. diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output index c7e6fb3bd770b..a7d39fb4308ef 100644 --- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output +++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output @@ -506,6 +506,9 @@ If the file name isn't specified the output file name is: '.bin': Parameters: --enabled - Filter only enabled events. + Reload TLS certificates on all server nodes by re-reading the configured key and trust stores. New connections use the updated certificates while established sessions are not interrupted: + control.(sh|bat) --ssl reload + By default commands affecting the cluster require interactive confirmation. Use --yes option to disable it.