You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2021/04/29 13:30:12 UTC

[nifi] branch main updated: NIFI-7134: Adding auto-reloading of Keystore and Truststore

This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 54a0e27  NIFI-7134: Adding auto-reloading of Keystore and Truststore
54a0e27 is described below

commit 54a0e27c937aeef98e17e999a6e61591a46bf91c
Author: Joe Gresock <jg...@gmail.com>
AuthorDate: Thu Apr 29 07:41:04 2021 -0400

    NIFI-7134: Adding auto-reloading of Keystore and Truststore
    
    - NIFI-7261 Included TrustStoreScanner for auto-reloading of truststore
    
    This closes #4991
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../java/org/apache/nifi/util/NiFiProperties.java  |  19 +++
 .../src/main/asciidoc/administration-guide.adoc    |  15 ++
 .../nifi-framework/nifi-resources/pom.xml          |   2 +
 .../src/main/resources/conf/nifi.properties        |   2 +
 .../org/apache/nifi/web/server/JettyServer.java    |  28 ++++
 .../nifi/web/server/util/TrustStoreScanner.java    | 151 +++++++++++++++++++++
 .../web/server/util/TrustStoreScannerTest.java     |  94 +++++++++++++
 7 files changed, 311 insertions(+)

diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index d4da56e..7370fa6 100644
--- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -153,6 +153,8 @@ public abstract class NiFiProperties {
     public static final String SECURITY_TRUSTSTORE = "nifi.security.truststore";
     public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.security.truststoreType";
     public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.security.truststorePasswd";
+    public static final String SECURITY_AUTO_RELOAD_ENABLED = "nifi.security.autoreload.enabled";
+    public static final String SECURITY_AUTO_RELOAD_INTERVAL = "nifi.security.autoreload.interval";
     public static final String SECURITY_USER_AUTHORIZER = "nifi.security.user.authorizer";
     public static final String SECURITY_ANONYMOUS_AUTHENTICATION = "nifi.security.allow.anonymous.authentication";
     public static final String SECURITY_USER_LOGIN_IDENTITY_PROVIDER = "nifi.security.user.login.identity.provider";
@@ -328,6 +330,7 @@ public abstract class NiFiProperties {
     public static final String DEFAULT_ZOOKEEPER_AUTH_TYPE = "default";
     public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_HOST_FROM_PRINCIPAL = "true";
     public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_REALM_FROM_PRINCIPAL = "true";
+    public static final String DEFAULT_SECURITY_AUTO_RELOAD_INTERVAL = "10 secs";
     public static final String DEFAULT_SITE_TO_SITE_HTTP_TRANSACTION_TTL = "30 secs";
     public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED = "true";
     public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = "30 days";
@@ -762,6 +765,22 @@ public abstract class NiFiProperties {
         return getProperty(UI_AUTO_REFRESH_INTERVAL);
     }
 
+    /**
+     * Returns true if auto reload of the keystore and truststore is enabled.
+     * @return true if auto reload of the keystore and truststore is enabled.
+     */
+    public boolean isSecurityAutoReloadEnabled() {
+        return this.getProperty(SECURITY_AUTO_RELOAD_ENABLED, Boolean.FALSE.toString()).equals(Boolean.TRUE.toString());
+    }
+
+    /**
+     * Returns the auto reload interval of the keystore and truststore.
+     * @return The interval over which the keystore and truststore should auto-reload.
+     */
+    public String getSecurityAutoReloadInterval() {
+        return getProperty(SECURITY_AUTO_RELOAD_INTERVAL, DEFAULT_SECURITY_AUTO_RELOAD_INTERVAL);
+    }
+
     // getters for cluster protocol properties //
     public String getClusterProtocolHeartbeatInterval() {
         return getProperty(CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL,
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index e4ab517..2c306bf 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -199,6 +199,19 @@ Now that the User Interface has been secured, we can easily secure Site-to-Site
 accomplished by setting the `nifi.remote.input.secure` and `nifi.cluster.protocol.is.secure` properties, respectively, to `true`. These communications
 will always REQUIRE two way SSL as the nodes will use their configured keystore/truststore for authentication.
 
+Automatic refreshing of NiFi's web SSL context factory can be enabled using the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`nifi.security.autoreload.enabled`|Specifies whether the SSL context factory should be automatically reloaded if updates to the keystore and truststore are detected. By default, it is set to `false`.
+|`nifi.security.autoreload.interval`|Specifies the interval at which the keystore and truststore are checked for updates. Only applies if `nifi.security.autoreload.enabled` is set to `true`. The default value is `10 secs`.
+|==================================================================================================================================================
+
+Once the `nifi.security.autoreload.enabled` property is set to `true`, any valid changes to the configured keystore and truststore will cause NiFi's SSL context factory to be reloaded, allowing clients to pick up the changes.  This is intended to allow expired certificates to be updated in the keystore and new trusted certificates to be added in the truststore, all without having to restart the NiFi server.
+
+NOTE: Changes to any of the `nifi.security.keystore*` or `nifi.security.truststore*` properties will not be picked up by the auto-refreshing logic, which assumes the passwords and store paths will remain the same.
+
 [[tls_generation_toolkit]]
 === TLS Generation Toolkit
 
@@ -3560,6 +3573,8 @@ These properties pertain to various security features in NiFi. Many of these pro
 |`nifi.sensitive.props.algorithm`|The algorithm used to encrypt sensitive properties. The default value is `PBEWITHMD5AND256BITAES-CBC-OPENSSL`.
 |`nifi.sensitive.props.provider`|The sensitive property provider. The default value is `BC`.
 |`nifi.sensitive.props.additional.keys`|The comma separated list of properties in _nifi.properties_ to encrypt in addition to the default sensitive properties (see <<encrypt-config_tool>>).
+|`nifi.security.autoreload.enabled`|Specifies whether the SSL context factory should be automatically reloaded if updates to the keystore and truststore are detected. By default, it is set to `false`.
+|`nifi.security.autoreload.interval`|Specifies the interval at which the keystore and truststore are checked for updates. Only applies if `nifi.security.autoreload.enabled` is set to `true`. The default value is `10 secs`.
 |`nifi.security.keystore`*|The full path and name of the keystore. It is blank by default.
 |`nifi.security.keystoreType`|The keystore type. It is blank by default.
 |`nifi.security.keystorePasswd`|The keystore password. It is blank by default.
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
index 43eed61..29831d5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
@@ -148,6 +148,8 @@
         <nifi.web.request.ip.whitelist />
         <nifi.web.should.send.server.version>true</nifi.web.should.send.server.version>
         <!-- nifi.properties: security properties -->
+        <nifi.security.autoreload.enabled>false</nifi.security.autoreload.enabled>
+        <nifi.security.autoreload.interval>10 secs</nifi.security.autoreload.interval>
         <!-- Use these values once we change default configuration to be HTTPS
         <nifi.security.keystore>./conf/keystore.p12</nifi.security.keystore>
         <nifi.security.keystoreType>PKCS12</nifi.security.keystoreType> -->
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
index e0a47d3..12baa86 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
@@ -178,6 +178,8 @@ nifi.sensitive.props.algorithm=${nifi.sensitive.props.algorithm}
 nifi.sensitive.props.provider=${nifi.sensitive.props.provider}
 nifi.sensitive.props.additional.keys=${nifi.sensitive.props.additional.keys}
 
+nifi.security.autoreload.enabled=${nifi.security.autoreload.enabled}
+nifi.security.autoreload.interval=${nifi.security.autoreload.interval}
 nifi.security.keystore=${nifi.security.keystore}
 nifi.security.keystoreType=${nifi.security.keystoreType}
 nifi.security.keystorePasswd=${nifi.security.keystorePasswd}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
index 16fa44c..3804ea7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
@@ -58,6 +58,7 @@ import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
 import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
 import org.apache.nifi.web.security.headers.XSSProtectionFilter;
 import org.apache.nifi.web.security.requests.ContentLengthFilter;
+import org.apache.nifi.web.server.util.TrustStoreScanner;
 import org.eclipse.jetty.annotations.AnnotationConfiguration;
 import org.eclipse.jetty.deploy.App;
 import org.eclipse.jetty.deploy.DeploymentManager;
@@ -77,6 +78,7 @@ import org.eclipse.jetty.servlet.DefaultServlet;
 import org.eclipse.jetty.servlet.FilterHolder;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.servlets.DoSFilter;
+import org.eclipse.jetty.util.ssl.KeyStoreScanner;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.webapp.Configuration;
@@ -150,6 +152,7 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
     private ExtensionMapping extensionMapping;
     private NarAutoLoader narAutoLoader;
     private DiagnosticsFactory diagnosticsFactory;
+    private SslContextFactory.Server sslContextFactory;
 
     private WebAppContext webApiContext;
     private WebAppContext webDocsContext;
@@ -315,6 +318,7 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
         return gzip(webAppContextHandlers);
     }
 
+
     @Override
     public void loadExtensionUis(final Set<Bundle> bundles) {
         // Find and load any WARs contained within the set of bundles...
@@ -880,6 +884,29 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
         ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port);
 
         configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc);
+
+        if (props.isSecurityAutoReloadEnabled()) {
+            configureSslContextFactoryReloading(server);
+        }
+    }
+
+    /**
+     * Configures a KeyStoreScanner and TrustStoreScanner at the configured reload intervals.  This will
+     * reload the SSLContextFactory if any changes are detected to the keystore or truststore.
+     * @param server The Jetty server
+     */
+    private void configureSslContextFactoryReloading(Server server) {
+        final int scanIntervalSeconds = Double.valueOf(FormatUtils.getPreciseTimeDuration(
+                props.getSecurityAutoReloadInterval(), TimeUnit.SECONDS))
+                .intValue();
+
+        final KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory);
+        keyStoreScanner.setScanInterval(scanIntervalSeconds);
+        server.addBean(keyStoreScanner);
+
+        final TrustStoreScanner trustStoreScanner = new TrustStoreScanner(sslContextFactory);
+        trustStoreScanner.setScanInterval(scanIntervalSeconds);
+        server.addBean(trustStoreScanner);
     }
 
     /**
@@ -1008,6 +1035,7 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
     private SslContextFactory createSslContextFactory() {
         final SslContextFactory.Server serverContextFactory = new SslContextFactory.Server();
         configureSslContextFactory(serverContextFactory, props);
+        this.sslContextFactory = serverContextFactory;
         return serverContextFactory;
     }
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/util/TrustStoreScanner.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/util/TrustStoreScanner.java
new file mode 100644
index 0000000..22913ed
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/util/TrustStoreScanner.java
@@ -0,0 +1,151 @@
+/*
+ * 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.nifi.web.server.util;
+
+import org.eclipse.jetty.util.Scanner;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedOperation;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * <p>The {@link TrustStoreScanner} is used to monitor the TrustStore file used by the {@link SslContextFactory}.
+ * It will reload the {@link SslContextFactory} if it detects that the TrustStore file has been modified.</p>
+ * <p>
+ * Though it would have been more ideal to simply extend KeyStoreScanner and override the keystore resource
+ * with the truststore resource, KeyStoreScanner's constructor was written in a way that doesn't make this possible.
+ */
+public class TrustStoreScanner extends ContainerLifeCycle implements Scanner.DiscreteListener {
+    private static final Logger LOG = Log.getLogger(TrustStoreScanner.class);
+
+    private final SslContextFactory sslContextFactory;
+    private final File truststoreFile;
+    private final Scanner _scanner;
+
+    public TrustStoreScanner(SslContextFactory sslContextFactory) {
+        this.sslContextFactory = sslContextFactory;
+        try {
+            Resource truststoreResource = sslContextFactory.getTrustStoreResource();
+            File monitoredFile = truststoreResource.getFile();
+            if (monitoredFile == null || !monitoredFile.exists()) {
+                throw new IllegalArgumentException("truststore file does not exist");
+            }
+            if (monitoredFile.isDirectory()) {
+                throw new IllegalArgumentException("expected truststore file not directory");
+            }
+
+            if (truststoreResource.getAlias() != null) {
+                // this resource has an alias, use the alias, as that's what's returned in the Scanner
+                monitoredFile = new File(truststoreResource.getAlias());
+            }
+
+            truststoreFile = monitoredFile;
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Monitored Truststore File: {}", monitoredFile);
+            }
+        } catch (IOException e) {
+            throw new IllegalArgumentException("could not obtain truststore file", e);
+        }
+
+        File parentFile = truststoreFile.getParentFile();
+        if (!parentFile.exists() || !parentFile.isDirectory()) {
+            throw new IllegalArgumentException("error obtaining truststore dir");
+        }
+
+        _scanner = new Scanner();
+        _scanner.setScanDirs(Collections.singletonList(parentFile));
+        _scanner.setScanInterval(1);
+        _scanner.setReportDirs(false);
+        _scanner.setReportExistingFilesOnStartup(false);
+        _scanner.setScanDepth(1);
+        _scanner.addListener(this);
+        addBean(_scanner);
+    }
+
+    @Override
+    public void fileAdded(String filename) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("added {}", filename);
+        }
+
+        if (truststoreFile.toPath().toString().equals(filename)) {
+            reload();
+        }
+    }
+
+    @Override
+    public void fileChanged(String filename) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("changed {}", filename);
+        }
+
+        if (truststoreFile.toPath().toString().equals(filename)) {
+            reload();
+        }
+    }
+
+    @Override
+    public void fileRemoved(String filename) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("removed {}", filename);
+        }
+
+        if (truststoreFile.toPath().toString().equals(filename)) {
+            reload();
+        }
+    }
+
+    @ManagedOperation(value = "Scan for changes in the SSL Truststore", impact = "ACTION")
+    public void scan() {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("scanning");
+        }
+
+        _scanner.scan();
+        _scanner.scan();
+    }
+
+    @ManagedOperation(value = "Reload the SSL Truststore", impact = "ACTION")
+    public void reload() {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("reloading truststore file {}", truststoreFile);
+        }
+
+        try {
+            sslContextFactory.reload(scf -> {
+            });
+        } catch (Throwable t) {
+            LOG.warn("Truststore Reload Failed", t);
+        }
+    }
+
+    @ManagedAttribute("scanning interval to detect changes which need reloaded")
+    public int getScanInterval() {
+        return _scanner.getScanInterval();
+    }
+
+    public void setScanInterval(int scanInterval) {
+        _scanner.setScanInterval(scanInterval);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/util/TrustStoreScannerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/util/TrustStoreScannerTest.java
new file mode 100644
index 0000000..fe7e021
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/util/TrustStoreScannerTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.nifi.web.server.util;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.util.function.Consumer;
+
+public class TrustStoreScannerTest {
+
+    private TrustStoreScanner scanner;
+    private SslContextFactory sslContextFactory;
+    private static File keyStoreFile;
+    private static File trustStoreFile;
+
+    @BeforeClass
+    public static void initClass() throws GeneralSecurityException, IOException {
+        TlsConfiguration tlsConfiguration = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore();
+        keyStoreFile = Paths.get(tlsConfiguration.getKeystorePath()).toFile();
+        trustStoreFile = Paths.get(tlsConfiguration.getTruststorePath()).toFile();
+    }
+
+    @Before
+    public void init() throws IOException {
+        sslContextFactory = Mockito.mock(SslContextFactory.class);
+        Resource trustStoreResource = Mockito.mock(Resource.class);
+        Mockito.when(trustStoreResource.getFile()).thenReturn(trustStoreFile);
+        Mockito.when(sslContextFactory.getTrustStoreResource()).thenReturn(trustStoreResource);
+
+        scanner = new TrustStoreScanner(sslContextFactory);
+    }
+
+    @Test
+    public void fileAdded() throws Exception {
+        scanner.fileAdded(trustStoreFile.getAbsolutePath());
+
+        Mockito.verify(sslContextFactory).reload(ArgumentMatchers.any(Consumer.class));
+    }
+
+    @Test
+    public void fileChanged() throws Exception {
+        scanner.fileChanged(trustStoreFile.getAbsolutePath());
+
+        Mockito.verify(sslContextFactory).reload(ArgumentMatchers.any(Consumer.class));
+    }
+
+    @Test
+    public void fileRemoved() throws Exception {
+        scanner.fileRemoved(trustStoreFile.getAbsolutePath());
+
+        Mockito.verify(sslContextFactory).reload(ArgumentMatchers.any(Consumer.class));
+    }
+
+    @Test
+    public void reload() throws Exception {
+        scanner.reload();
+
+        Mockito.verify(sslContextFactory).reload(ArgumentMatchers.any(Consumer.class));
+    }
+
+    @AfterClass
+    public static void tearDown() throws IOException {
+        Files.deleteIfExists(keyStoreFile.toPath());
+        Files.deleteIfExists(trustStoreFile.toPath());
+    }
+}