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 <exceptionfactory@apache.org>
This commit is contained in:
Joe Gresock 2021-04-29 07:41:04 -04:00 committed by exceptionfactory
parent 93dcf25d33
commit 54a0e27c93
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
7 changed files with 311 additions and 0 deletions

View File

@ -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,

View File

@ -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.

View File

@ -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> -->

View File

@ -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}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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());
}
}