exceptionfactory commented on a change in pull request #4991:
URL: https://github.com/apache/nifi/pull/4991#discussion_r621736609



##########
File path: nifi-docs/src/main/asciidoc/administration-guide.adoc
##########
@@ -199,6 +199,26 @@ 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.autorefresh.enabled`|Specifies whether the SSL context factory 
should be automatically refreshed if updates to the keystore and truststore are 
detected. By default, it is set to `false`.
+|`nifi.security.autorefresh.interval`|Specifies the interval at which the 
keystore and truststore are checked for updates. Only applies if 
`nifi.security.autorefresh.enabled` is set to `true`. The default value is `10 
secs`.
+|==================================================================================================================================================
+
+Once the `nifi.security.autorefresh.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.
+
+There are no restrictions on updates to certificates in the trust store, but 
for security reasons, changes to the keystore are considered "valid" only if:
+
+* No entries are added to the keystore
+* There are no changes to the aliases of existing entries
+* There are no changes to the subject principal of existing entries
+* There are no changes to the issuer principal or serial number of existing 
entries

Review comment:
       On further review, do you think it is necessary to ensure the continuity 
both the alias and issuer?  Checking the subject ensures that the identity 
doesn't change, but the alias does not seem like it impacts functionality.  The 
issuer should generally remain the same, but it is possible that the subject 
could stay the same and a new Certificate Authority issued the certificate with 
the same subject.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+    private File keystore = null;
+    private File truststore = null;
+    private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+    private boolean isApplicable = false;
+
+    private long pollIntervalMS;
+    private final ScheduledThreadPoolExecutor executor;
+
+    private final NiFiProperties niFiProperties;
+
+    private volatile SSLContextFactoryAutoLoaderTask 
sslContextFactoryAutoLoaderTask;
+    private volatile boolean started = false;
+
+    public SSLContextFactoryAutoLoader(final NiFiProperties nifiProperties, 
SSLContextFactoryReloadable sslContextFactoryReloadable) {
+        this.niFiProperties = nifiProperties;
+        if 
(nifiProperties.getProperty(NiFiProperties.SECURITY_AUTO_REFRESH_ENABLED, 
"false").equals("true")) {
+            LOGGER.info("Auto-refreshing of keystore and truststore is 
enabled.");

Review comment:
       This informational log seems unnecessary since another log message is 
written at the end of this block based on whether or not the loader is enabled.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+    private File keystore = null;
+    private File truststore = null;
+    private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+    private boolean isApplicable = false;
+
+    private long pollIntervalMS;
+    private final ScheduledThreadPoolExecutor executor;
+
+    private final NiFiProperties niFiProperties;
+
+    private volatile SSLContextFactoryAutoLoaderTask 
sslContextFactoryAutoLoaderTask;
+    private volatile boolean started = false;

Review comment:
       Could this tracking variable be replaced by calling 
`ThreadPoolExecutor.isShutdown()`

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+    private File keystore = null;
+    private File truststore = null;
+    private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+    private boolean isApplicable = false;
+
+    private long pollIntervalMS;
+    private final ScheduledThreadPoolExecutor executor;
+
+    private final NiFiProperties niFiProperties;
+
+    private volatile SSLContextFactoryAutoLoaderTask 
sslContextFactoryAutoLoaderTask;
+    private volatile boolean started = false;
+
+    public SSLContextFactoryAutoLoader(final NiFiProperties nifiProperties, 
SSLContextFactoryReloadable sslContextFactoryReloadable) {
+        this.niFiProperties = nifiProperties;
+        if 
(nifiProperties.getProperty(NiFiProperties.SECURITY_AUTO_REFRESH_ENABLED, 
"false").equals("true")) {
+            LOGGER.info("Auto-refreshing of keystore and truststore is 
enabled.");
+            pollIntervalMS = 
Double.valueOf(FormatUtils.getPreciseTimeDuration(nifiProperties.getSecurityAutoRefreshInterval(),
 TimeUnit.MILLISECONDS))
+                    .longValue();
+
+            String keystorePath = 
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE);
+            if (StringUtils.isNotBlank(keystorePath)) {
+                this.keystore = new File(keystorePath);
+                if (keystore.exists() && keystore.canRead()) {
+                    String truststorePath = 
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE);
+                    if (StringUtils.isNotBlank(truststorePath)) {
+                        this.truststore = new File(truststorePath);
+
+                        if (truststore.exists() && truststore.canRead()) {
+                            isApplicable = true;
+                        }
+                    }
+                }
+            }
+            if (isApplicable) {
+                LOGGER.info("Keystore and truststore will auto-refresh when 
applicable changes are made.");
+            } else {
+                LOGGER.info("No keystore and truststore detected: nothing to 
auto-refresh.");
+            }
+        } else {
+            LOGGER.info("Auto-refreshing of keystore and truststore is 
disabled.");
+        }
+        this.executor = new ScheduledThreadPoolExecutor(1, new ThreadFactory() 
{
+            @Override
+            public Thread newThread(Runnable r) {
+                final Thread thread = new Thread(r);
+                thread.setName("SSL Context Factory Auto-Loader");
+                thread.setDaemon(true);
+                return thread;
+            }
+        });
+        this.sslContextFactoryReloadable = sslContextFactoryReloadable;
+    }
+
+    protected SSLContextFactoryReloadable getSslContextFactoryReloadable() {
+        return sslContextFactoryReloadable;
+    }
+
+    public synchronized void start() throws IOException, 
NoSuchAlgorithmException, UnrecoverableEntryException, KeyStoreException, 
TlsException {
+        if (started || !isApplicable) {
+            return;
+        }
+
+        final WatchService keystoreWatcher = 
FileSystems.getDefault().newWatchService();
+        final Path keystoreDirPath = keystore.toPath().getParent();
+        keystoreDirPath.register(keystoreWatcher, 
StandardWatchEventKinds.ENTRY_MODIFY);
+
+        WatchService truststoreWatcher = 
FileSystems.getDefault().newWatchService();
+        final Path truststoreDirPath = truststore.toPath().getParent();
+        truststoreDirPath.register(truststoreWatcher, 
StandardWatchEventKinds.ENTRY_MODIFY);
+
+        // In this case, just watch the same directory, but the task will look 
for both keystore and truststore file updates
+        if (truststoreDirPath.toString().equals(keystoreDirPath.toString())) {
+            truststoreWatcher = keystoreWatcher;
+        }
+
+        sslContextFactoryAutoLoaderTask = new 
SSLContextFactoryAutoLoaderTask.Builder()
+                .keystorePath(keystore.toPath())
+                .truststorePath(truststore.toPath())
+                .keystoreWatchService(keystoreWatcher)
+                .truststoreWatchService(truststoreWatcher)
+                .autoLoader(this)
+                .nifiProperties(niFiProperties)
+                .build();
+
+        LOGGER.info("Starting keystore auto-refresh for {} ...", new 
Object[]{keystore.toPath()});
+        LOGGER.info("Starting truststore auto-refresh for {} ...", new 
Object[]{truststore.toPath()});

Review comment:
       It seems like these messages would be better as debug, or perhaps 
removed, since the keystore and truststore are known properties, and the 
constructor would have already logged that fact the automatic refresh is 
enabled.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+    private File keystore = null;
+    private File truststore = null;
+    private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+    private boolean isApplicable = false;
+
+    private long pollIntervalMS;
+    private final ScheduledThreadPoolExecutor executor;
+
+    private final NiFiProperties niFiProperties;
+
+    private volatile SSLContextFactoryAutoLoaderTask 
sslContextFactoryAutoLoaderTask;
+    private volatile boolean started = false;
+
+    public SSLContextFactoryAutoLoader(final NiFiProperties nifiProperties, 
SSLContextFactoryReloadable sslContextFactoryReloadable) {
+        this.niFiProperties = nifiProperties;
+        if 
(nifiProperties.getProperty(NiFiProperties.SECURITY_AUTO_REFRESH_ENABLED, 
"false").equals("true")) {
+            LOGGER.info("Auto-refreshing of keystore and truststore is 
enabled.");
+            pollIntervalMS = 
Double.valueOf(FormatUtils.getPreciseTimeDuration(nifiProperties.getSecurityAutoRefreshInterval(),
 TimeUnit.MILLISECONDS))
+                    .longValue();
+
+            String keystorePath = 
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE);
+            if (StringUtils.isNotBlank(keystorePath)) {
+                this.keystore = new File(keystorePath);
+                if (keystore.exists() && keystore.canRead()) {
+                    String truststorePath = 
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE);
+                    if (StringUtils.isNotBlank(truststorePath)) {
+                        this.truststore = new File(truststorePath);
+
+                        if (truststore.exists() && truststore.canRead()) {
+                            isApplicable = true;
+                        }
+                    }
+                }
+            }
+            if (isApplicable) {
+                LOGGER.info("Keystore and truststore will auto-refresh when 
applicable changes are made.");

Review comment:
       Although minor, recommend avoiding periods at the end of logging 
statements, perhaps simplifying it to the following?
   ```suggestion
                   LOGGER.info("Automatic refresh of keystore and truststore 
enabled");
   ```

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {
+
+                    final Path autoLoadFile = 
storePath.getParent().resolve(filename);
+                    final String autoLoadFilename = 
autoLoadFile.toFile().getName();
+
+                    if 
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+                        continue;
+                    }
+
+                    LOGGER.info("Found update to {}", new 
Object[]{autoLoadFilename});
+                    storeChanged = true;
+                }
+            }
+
+            final boolean valid = key.reset();
+            if (!valid) {
+                LOGGER.error("{} auto-refresh directory is no longer valid", 
new Object[] {storePaths.iterator().next()});
+                autoLoader.stop();
+            }
+            return storeChanged;
+        }
+        return false;
+    }
+
+    @Override
+    public void run() {
+        Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath, 
truststorePath));
+        try {
+            boolean storeChanged = false;
+            // Can we poll the same directory for updates?
+            if (keystoreWatchService == truststoreWatchService) {
+                LOGGER.debug("Polling for keystore updates at {} and 
truststore updates at {}", new Object[]{keystorePath, truststorePath});
+                storeChanged = this.poll(keystoreWatchService, bothPaths);
+            } else {
+                // Otherwise, poll separate directories
+                LOGGER.debug("Polling for keystore updates at {}", new 
Object[]{keystorePath});
+                storeChanged = this.poll(keystoreWatchService, 
Arrays.asList(keystorePath));
+
+                LOGGER.debug("Polling for truststore updates at {}", new 
Object[]{truststorePath});
+                storeChanged |= this.poll(truststoreWatchService, 
Arrays.asList(truststorePath));
+            }
+
+            if (storeChanged) {
+                if (this.isReloadAllowed()) {
+                    
autoLoader.getSslContextFactoryReloadable().reloadSslContextFactory();
+                } else {
+                    LOGGER.warn("For security reasons, the SSL Context Factory 
could not be reloaded because the " +
+                            "keystore {} changed in a way that is 
disallowed.", new Object[] {keystorePath});
+                }
+            }
+
+        } catch (final Throwable t) {
+            LOGGER.error("Error reloading SSL context factory due to: " + 
t.getMessage(), t);

Review comment:
       String concatenation should be replaced with placeholder logging 
statements.
   ```suggestion
               LOGGER.error("Reloading SslContextFactory Failed: {}", 
t.getMessage(), t);
   ```

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {
+
+                    final Path autoLoadFile = 
storePath.getParent().resolve(filename);
+                    final String autoLoadFilename = 
autoLoadFile.toFile().getName();
+
+                    if 
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+                        continue;
+                    }
+
+                    LOGGER.info("Found update to {}", new 
Object[]{autoLoadFilename});
+                    storeChanged = true;
+                }
+            }
+
+            final boolean valid = key.reset();
+            if (!valid) {
+                LOGGER.error("{} auto-refresh directory is no longer valid", 
new Object[] {storePaths.iterator().next()});
+                autoLoader.stop();
+            }
+            return storeChanged;
+        }
+        return false;
+    }
+
+    @Override
+    public void run() {
+        Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath, 
truststorePath));
+        try {
+            boolean storeChanged = false;
+            // Can we poll the same directory for updates?
+            if (keystoreWatchService == truststoreWatchService) {

Review comment:
       Should this use `equals()` instead of `==`?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {
+
+                    final Path autoLoadFile = 
storePath.getParent().resolve(filename);
+                    final String autoLoadFilename = 
autoLoadFile.toFile().getName();
+
+                    if 
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+                        continue;
+                    }
+
+                    LOGGER.info("Found update to {}", new 
Object[]{autoLoadFilename});
+                    storeChanged = true;
+                }
+            }
+
+            final boolean valid = key.reset();
+            if (!valid) {
+                LOGGER.error("{} auto-refresh directory is no longer valid", 
new Object[] {storePaths.iterator().next()});
+                autoLoader.stop();
+            }
+            return storeChanged;
+        }
+        return false;
+    }
+
+    @Override
+    public void run() {
+        Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath, 
truststorePath));
+        try {
+            boolean storeChanged = false;
+            // Can we poll the same directory for updates?
+            if (keystoreWatchService == truststoreWatchService) {
+                LOGGER.debug("Polling for keystore updates at {} and 
truststore updates at {}", new Object[]{keystorePath, truststorePath});

Review comment:
       The `Object[]` wrapper here and following is not necessary:
   ```suggestion
                   LOGGER.debug("Polling keystore [{}] truststore [{}] for 
updates", keystorePath, truststorePath);
   ```

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {

Review comment:
       What do you think of renaming this to `SslContextFactoryMonitor`?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {
+
+                    final Path autoLoadFile = 
storePath.getParent().resolve(filename);
+                    final String autoLoadFilename = 
autoLoadFile.toFile().getName();
+
+                    if 
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+                        continue;
+                    }
+
+                    LOGGER.info("Found update to {}", new 
Object[]{autoLoadFilename});

Review comment:
       This seems like a debug statement instead of info.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {

Review comment:
       Is it possible for `storePaths` to be empty since this is a private 
method?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+    private File keystore = null;
+    private File truststore = null;
+    private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+    private boolean isApplicable = false;
+
+    private long pollIntervalMS;
+    private final ScheduledThreadPoolExecutor executor;
+
+    private final NiFiProperties niFiProperties;
+
+    private volatile SSLContextFactoryAutoLoaderTask 
sslContextFactoryAutoLoaderTask;

Review comment:
       Is it necessary to keep this as a member variable, or can it be changed 
to method-local?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {
+
+                    final Path autoLoadFile = 
storePath.getParent().resolve(filename);
+                    final String autoLoadFilename = 
autoLoadFile.toFile().getName();
+
+                    if 
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+                        continue;
+                    }
+
+                    LOGGER.info("Found update to {}", new 
Object[]{autoLoadFilename});
+                    storeChanged = true;
+                }
+            }
+
+            final boolean valid = key.reset();
+            if (!valid) {
+                LOGGER.error("{} auto-refresh directory is no longer valid", 
new Object[] {storePaths.iterator().next()});
+                autoLoader.stop();
+            }
+            return storeChanged;
+        }
+        return false;
+    }
+
+    @Override
+    public void run() {
+        Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath, 
truststorePath));
+        try {
+            boolean storeChanged = false;
+            // Can we poll the same directory for updates?
+            if (keystoreWatchService == truststoreWatchService) {
+                LOGGER.debug("Polling for keystore updates at {} and 
truststore updates at {}", new Object[]{keystorePath, truststorePath});
+                storeChanged = this.poll(keystoreWatchService, bothPaths);
+            } else {
+                // Otherwise, poll separate directories
+                LOGGER.debug("Polling for keystore updates at {}", new 
Object[]{keystorePath});
+                storeChanged = this.poll(keystoreWatchService, 
Arrays.asList(keystorePath));
+
+                LOGGER.debug("Polling for truststore updates at {}", new 
Object[]{truststorePath});
+                storeChanged |= this.poll(truststoreWatchService, 
Arrays.asList(truststorePath));
+            }
+
+            if (storeChanged) {
+                if (this.isReloadAllowed()) {
+                    
autoLoader.getSslContextFactoryReloadable().reloadSslContextFactory();
+                } else {
+                    LOGGER.warn("For security reasons, the SSL Context Factory 
could not be reloaded because the " +
+                            "keystore {} changed in a way that is 
disallowed.", new Object[] {keystorePath});
+                }
+            }
+
+        } catch (final Throwable t) {
+            LOGGER.error("Error reloading SSL context factory due to: " + 
t.getMessage(), t);
+        }
+    }
+
+    /**
+     * Returns a list representing the state of the current keystore at the 
given path.  This method uses the
+     * keystore properties from nifi.properties.  The only state retrieved 
will be the alias, subject DN,
+     * issuer DN of each PrivateKeyEntry, and issuer certificate serial number 
if applicable, and the results
+     * will be a sorted list.
+     * @return A sorted list of information about each private key in the 
keystore
+     * @throws TlsException If the keystore could not be loaded
+     * @throws KeyStoreException If the keystore password was incorrect
+     * @throws UnrecoverableEntryException If a private key entry could not be 
recovered
+     * @throws NoSuchAlgorithmException If the default password algorithm is 
not supported

Review comment:
       Declaring all of these exceptions impacts several other methods, and 
handling them separately doesn't provide much value.  What do you think about 
adjusting the signature of this and other methods to catch these checked 
exceptions and wrap them in one checked exception, or perhaps a subclass of 
`RuntimeException`?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {

Review comment:
       Recommend adding spacing and `final` keyword:
   ```suggestion
                   for (final Path storePath : storePaths) {
   ```

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
##########
@@ -315,6 +319,15 @@ private Handler loadInitialWars(final Set<Bundle> bundles) 
{
         return gzip(webAppContextHandlers);
     }
 
+    @Override
+    public void reloadSslContextFactory() throws Exception {

Review comment:
       Instead of defining the new `SSLContextFactoryReloadable` and 
implementing this method, why not just pass the Jetty `SslContextFactory` to 
the loader class?  That would reduce the impact on the JettyServer class.  This 
also follows the approach of the Jetty 
[KeyStoreScanner](https://github.com/eclipse/jetty.project/blob/b56edf511ab4399122ea2c6162a4a5988870f479/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java).

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore 
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+    private static final int MIN_FILE_AGE = 5000;
+
+    private final Path keystorePath;
+    private final Path truststorePath;
+    private final WatchService keystoreWatchService;
+    private final WatchService truststoreWatchService;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<CertificateEntryDescription> existingKeystoreState;
+
+    private volatile boolean stopped = false;
+
+    private SSLContextFactoryAutoLoaderTask(final Builder builder) throws 
NoSuchAlgorithmException, UnrecoverableEntryException,
+            KeyStoreException, TlsException {
+        this.keystorePath = builder.keystorePath;
+        this.truststorePath = builder.truststorePath;
+        this.keystoreWatchService = builder.keystoreWatchService;
+        this.truststoreWatchService = builder.truststoreWatchService;
+        this.autoLoader = builder.autoLoader;
+        this.nifiProperties = builder.niFiProperties;
+        this.existingKeystoreState = this.getKeystoreState();
+        this.candidateStores = new ArrayList<>();
+    }
+
+    private boolean poll(WatchService watchService, Collection<Path> 
storePaths) {
+        if (storePaths == null || storePaths.isEmpty()) {
+            throw new RuntimeException("A polling directory must be 
specified.");
+        }
+        WatchKey key = watchService.poll();
+
+        boolean storeChanged = false;
+
+        // Key comes back as null when there are no new create events, but we 
still want to continue processing
+        // so we can consider files added to the candidateNars list in 
previous iterations
+
+        if (key != null) {
+            for (WatchEvent<?> event : key.pollEvents()) {
+                final WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+                final Path filename = ev.context();
+
+                for(Path storePath : storePaths) {
+
+                    final Path autoLoadFile = 
storePath.getParent().resolve(filename);
+                    final String autoLoadFilename = 
autoLoadFile.toFile().getName();
+
+                    if 
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+                        continue;
+                    }
+
+                    LOGGER.info("Found update to {}", new 
Object[]{autoLoadFilename});
+                    storeChanged = true;
+                }
+            }
+
+            final boolean valid = key.reset();
+            if (!valid) {
+                LOGGER.error("{} auto-refresh directory is no longer valid", 
new Object[] {storePaths.iterator().next()});
+                autoLoader.stop();
+            }
+            return storeChanged;
+        }
+        return false;
+    }
+
+    @Override
+    public void run() {
+        Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath, 
truststorePath));
+        try {
+            boolean storeChanged = false;
+            // Can we poll the same directory for updates?
+            if (keystoreWatchService == truststoreWatchService) {
+                LOGGER.debug("Polling for keystore updates at {} and 
truststore updates at {}", new Object[]{keystorePath, truststorePath});
+                storeChanged = this.poll(keystoreWatchService, bothPaths);
+            } else {
+                // Otherwise, poll separate directories
+                LOGGER.debug("Polling for keystore updates at {}", new 
Object[]{keystorePath});
+                storeChanged = this.poll(keystoreWatchService, 
Arrays.asList(keystorePath));
+
+                LOGGER.debug("Polling for truststore updates at {}", new 
Object[]{truststorePath});
+                storeChanged |= this.poll(truststoreWatchService, 
Arrays.asList(truststorePath));
+            }
+
+            if (storeChanged) {
+                if (this.isReloadAllowed()) {
+                    
autoLoader.getSslContextFactoryReloadable().reloadSslContextFactory();
+                } else {
+                    LOGGER.warn("For security reasons, the SSL Context Factory 
could not be reloaded because the " +
+                            "keystore {} changed in a way that is 
disallowed.", new Object[] {keystorePath});
+                }
+            }
+
+        } catch (final Throwable t) {
+            LOGGER.error("Error reloading SSL context factory due to: " + 
t.getMessage(), t);
+        }
+    }
+
+    /**
+     * Returns a list representing the state of the current keystore at the 
given path.  This method uses the
+     * keystore properties from nifi.properties.  The only state retrieved 
will be the alias, subject DN,
+     * issuer DN of each PrivateKeyEntry, and issuer certificate serial number 
if applicable, and the results
+     * will be a sorted list.
+     * @return A sorted list of information about each private key in the 
keystore
+     * @throws TlsException If the keystore could not be loaded
+     * @throws KeyStoreException If the keystore password was incorrect
+     * @throws UnrecoverableEntryException If a private key entry could not be 
recovered
+     * @throws NoSuchAlgorithmException If the default password algorithm is 
not supported
+     */
+    private List<CertificateEntryDescription> getKeystoreState() throws 
TlsException, KeyStoreException,
+            UnrecoverableEntryException, NoSuchAlgorithmException {
+        List<CertificateEntryDescription> state = new ArrayList<>();
+
+        KeyStore keyStore = KeyStoreUtils.loadKeyStore(keystorePath.toString(),
+                
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD).toCharArray(),
+                
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE));
+
+        Enumeration<String> aliasesEnum = keyStore.aliases();
+        while(aliasesEnum.hasMoreElements()) {
+            String alias = aliasesEnum.nextElement();
+            if (keyStore.isKeyEntry(alias)) {
+                KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) 
keyStore.getEntry(alias, new KeyStore.PasswordProtection(nifiProperties
+                        
.getProperty(NiFiProperties.SECURITY_KEY_PASSWD).toCharArray()));
+                X509Certificate cert = (X509Certificate) 
entry.getCertificateChain()[0];
+                String issuerSerialNumber = 
(entry.getCertificateChain().length > 1)
+                        ? ((X509Certificate) 
entry.getCertificateChain()[1]).getSerialNumber().toString()
+                        : null;
+                state.add(new 
CertificateEntryDescription(cert.getSubjectX500Principal(), 
cert.getIssuerX500Principal(), alias, issuerSerialNumber));
+            }
+        }
+
+        Collections.sort(state);
+        return new ArrayList<>(state);
+    }
+
+    /**
+     * This returns false if there were any changes to the keystore other than 
updating a PrivateKeyEntry with
+     * the same subject DN, issuer DN, alias, and issuer cert serial number if 
applicable.
+     * @return True if a reload should be allowed, meaning the keystore has 
not changed in a meaningful way
+     */
+    boolean isReloadAllowed() throws NoSuchAlgorithmException, 
UnrecoverableEntryException, KeyStoreException, TlsException {
+        return existingKeystoreState.equals(this.getKeystoreState());
+    }
+
+    /**
+     * Builder for SSLContextFactoryAutoLoaderTask.
+     */
+    public static class Builder {

Review comment:
       The builder pattern is useful for optional properties, but there it 
looks like all of the properties should always be provided.  Is there another 
reason for using the builder pattern as opposed to passing everything as 
constructor arguments?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in 
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+    private File keystore = null;
+    private File truststore = null;
+    private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+    private boolean isApplicable = false;

Review comment:
       Perhaps changing this to `applicable` or simply `enabled` would be a bit 
a cleaner.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/test/java/org/apache/nifi/autoload/TestSSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,197 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.StandardTlsConfiguration;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.GeneralSecurityException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.Arrays;
+
+public class TestSSLContextFactoryAutoLoaderTask {
+
+    private static final Path KEYSTORE = new 
File("src/test/resources/keystore.jks").toPath();
+    private static final Path TRUSTSTORE = new 
File("src/test/resources/truststore.jks").toPath();
+    private static final String KEYSTORE_PASSWORD = "testtesttest";
+    private static final String KEY_PASSWORD = "testtesttest";
+    private static final String KEYSTORE_TYPE = "JKS";
+
+    private static final String DEFAULT_KEY_ALIAS = "nifi-key";
+    private static final String DEFAULT_CERT_DN = "CN=localhost, OU=NIFI";
+
+    private SSLContextFactoryAutoLoaderTask task;
+    private NiFiProperties nifiProperties;
+    private SSLContextFactoryAutoLoader autoLoader;
+    private SSLContextFactoryReloadable sslContextFactoryReloadable;
+    private WatchService keystoreWatchService;
+    private WatchService truststoreWatchService;
+
+    @Before
+    public void init() throws GeneralSecurityException, IOException {
+        this.createKeystore(DEFAULT_KEY_ALIAS, DEFAULT_CERT_DN);
+
+        nifiProperties = Mockito.mock(NiFiProperties.class);
+        
Mockito.when(nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD)).thenReturn(KEYSTORE_PASSWORD);
+        
Mockito.when(nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD)).thenReturn(KEY_PASSWORD);
+        
Mockito.when(nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE)).thenReturn(KEYSTORE_TYPE);
+
+        keystoreWatchService = Mockito.mock(WatchService.class);
+        truststoreWatchService = Mockito.mock(WatchService.class);
+        autoLoader = Mockito.mock(SSLContextFactoryAutoLoader.class);
+        sslContextFactoryReloadable = 
Mockito.mock(SSLContextFactoryReloadable.class);
+        
Mockito.when(autoLoader.getSslContextFactoryReloadable()).thenReturn(sslContextFactoryReloadable);
+
+        task = new SSLContextFactoryAutoLoaderTask.Builder()
+                .keystorePath(KEYSTORE)
+                .keystoreWatchService(keystoreWatchService)
+                .truststorePath(TRUSTSTORE)
+                .truststoreWatchService(truststoreWatchService)
+                .autoLoader(autoLoader)
+                .nifiProperties(nifiProperties)
+                .build();
+    }
+
+    @Test
+    public void testIsReloadAllowed_true() throws IOException, 
GeneralSecurityException {
+        this.createKeystore(DEFAULT_KEY_ALIAS, DEFAULT_CERT_DN); // Creates a 
keystore with the same DNs and alias, so this is allowed
+        Assert.assertTrue(task.isReloadAllowed());
+    }
+
+    @Test
+    public void testIsReloadAllowed_differentAlias() throws IOException, 
GeneralSecurityException {
+        this.createKeystore("different-alias", DEFAULT_CERT_DN);
+        Assert.assertFalse(task.isReloadAllowed());
+    }
+
+    @Test
+    public void testIsReloadAllowed_differentSubjectDN() throws IOException, 
GeneralSecurityException {
+        this.createKeystore(DEFAULT_KEY_ALIAS, "CN=different");
+        Assert.assertFalse(task.isReloadAllowed());
+    }
+
+    @Test
+    public void testIsReloadAllowed_differentIssuerSerialNumber() throws 
IOException, UnrecoverableEntryException, NoSuchAlgorithmException, 
KeyStoreException, TlsException {
+        // This is a keystore with the same key alias and cert DN, but with a 
different issuer cert in the cert chain,
+        // showing that we disallow cert updates with the same issuer DN but 
different actual issuer serial number
+        Files.copy(new File("src/test/resources/test-keystore.jks").toPath(), 
KEYSTORE, StandardCopyOption.REPLACE_EXISTING);

Review comment:
       Instead of committing the `test-keystore.jks` binary, can this be 
replaced with a generated keystore as used in other methods?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
##########
@@ -315,6 +319,15 @@ private Handler loadInitialWars(final Set<Bundle> bundles) 
{
         return gzip(webAppContextHandlers);
     }
 
+    @Override
+    public void reloadSslContextFactory() throws Exception {
+        if (sslContextFactory != null) {
+            this.sslContextFactory.reload(s -> {
+                logger.info("Successfully reloaded SSLContextFactory");

Review comment:
       Recommend adjusting log message to follow the class name more precisely:
   ```suggestion
                   logger.info("SslContextFactory reloaded");
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


Reply via email to