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/control-utility/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java b/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java index 4976c53d37784..ef07713a6b2d9 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/CommandHandlerParsingTest.java @@ -79,6 +79,7 @@ import org.apache.ignite.internal.management.property.PropertyCommand; import org.apache.ignite.internal.management.snapshot.SnapshotCommand; import org.apache.ignite.internal.management.snapshot.SnapshotRestoreCommand; +import org.apache.ignite.internal.management.ssl.SslCommand; import org.apache.ignite.internal.management.tx.TxCommand; import org.apache.ignite.internal.management.tx.TxCommandArg; import org.apache.ignite.internal.management.tx.TxSortOrder; @@ -1398,6 +1399,7 @@ private boolean requireArgs(Class cmd) { cmd == PerformanceStatisticsCommand.class || cmd == ConsistencyCommand.class || cmd == CdcCommand.class || - cmd == EventCommand.class; + cmd == EventCommand.class || + cmd == SslCommand.class; } } 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 808437eb1719a..057ed76380b12 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; @@ -504,6 +510,8 @@ else if (connCtx.managementClient()) { sslFilter.wantClientAuth(auth); sslFilter.needClientAuth(auth); + this.sslFilter = sslFilter; + return new GridNioFilter[] { openSesFilter, codecFilter, @@ -518,6 +526,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 d88253978062b..d2c73d786d9d9 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 @@ -51,6 +51,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; @@ -62,6 +63,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; @@ -104,10 +111,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(); @@ -174,6 +180,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. * @@ -227,6 +250,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/spi/communication/tcp/TcpCommunicationSpi.java b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationSpi.java index 216219cb5a6e5..07aed38658fc2 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"; @@ -422,6 +423,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 eb90f002a240f..5f7bcc9ddf2d7 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 @@ -103,6 +103,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; @@ -219,6 +220,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; @@ -722,6 +726,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. @@ -923,6 +951,8 @@ private MessageFactory get() { sslFilter.needClientAuth(true); filters.add(sslFilter); + + this.sslFilter = sslFilter; } GridNioFilter[] filtersArr = filters.toArray(new GridNioFilter[filters.size()]); 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. diff --git a/modules/nio/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java b/modules/nio/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java index 9abcae2ad56a4..7b31958250f4d 100644 --- a/modules/nio/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java +++ b/modules/nio/src/main/java/org/apache/ignite/internal/util/nio/ssl/GridNioSslFilter.java @@ -68,8 +68,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; @@ -117,6 +117,16 @@ public GridNioSslFilter( this.rejectedSesCnt = rejectedSesCnt; } + /** + * 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/nio/src/main/java/org/apache/ignite/internal/util/nio/ssl/SslContextReloadable.java b/modules/nio/src/main/java/org/apache/ignite/internal/util/nio/ssl/SslContextReloadable.java new file mode 100644 index 0000000000000..5616bc674e092 --- /dev/null +++ b/modules/nio/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; +}