Skip to content

Commit

Permalink
NIFI-11536 Corrected Keystore and Truststore auto-reloading
Browse files Browse the repository at this point in the history
- Replaced Jetty KeyStoreScanner and custom TrustStoreScanner with shared StoreScanner
- New StoreScanner uses TLS Configuration to reload SSLContext instead of relying on Jetty SslContextFactory properties

This closes #7446

Signed-off-by: David Handermann <exceptionfactory@apache.org>
(cherry picked from commit a85ef2c)
  • Loading branch information
emiliosetiadarma authored and exceptionfactory committed Jul 10, 2023
1 parent 9161485 commit 3649ade
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 247 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@
import org.apache.nifi.security.util.TlsPlatform;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.server.util.TrustStoreScanner;
import org.apache.nifi.web.server.util.StoreScanner;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
import org.eclipse.jetty.util.ssl.SslContextFactory;

import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -149,12 +148,12 @@ protected SslContextFactory.Server getSslContextFactory() {

if (storeScanInterval != null) {
sslContextFactory.setKeyStorePath(tlsConfiguration.getKeystorePath());
final KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory);
final StoreScanner keyStoreScanner = new StoreScanner(sslContextFactory, tlsConfiguration, sslContextFactory.getKeyStoreResource());
keyStoreScanner.setScanInterval(storeScanInterval);
getServer().addBean(keyStoreScanner);

sslContextFactory.setTrustStorePath(tlsConfiguration.getTruststorePath());
final TrustStoreScanner trustStoreScanner = new TrustStoreScanner(sslContextFactory);
final StoreScanner trustStoreScanner = new StoreScanner(sslContextFactory, tlsConfiguration, sslContextFactory.getTrustStoreResource());
trustStoreScanner.setScanInterval(storeScanInterval);
getServer().addBean(trustStoreScanner);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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.TlsConfiguration;
import org.apache.nifi.security.util.TlsException;
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 javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.util.Collections;

import static org.apache.nifi.security.util.SslContextFactory.createSslContext;

/**
* File Scanner for Keystore or Truststore reloading using provided TLS Configuration
*/
public class StoreScanner extends ContainerLifeCycle implements Scanner.DiscreteListener {
private static final Logger LOG = Log.getLogger(StoreScanner.class);

private final SslContextFactory sslContextFactory;
private final TlsConfiguration tlsConfiguration;
private final File file;
private final Scanner scanner;
private final String resourceName;

public StoreScanner(final SslContextFactory sslContextFactory,
final TlsConfiguration tlsConfiguration,
final Resource resource) {
this.sslContextFactory = sslContextFactory;
this.tlsConfiguration = tlsConfiguration;
this.resourceName = resource.getName();
try {
File monitoredFile = resource.getFile();
if (monitoredFile == null || !monitoredFile.exists()) {
throw new IllegalArgumentException(String.format("%s file does not exist", resourceName));
}
if (monitoredFile.isDirectory()) {
throw new IllegalArgumentException(String.format("expected %s file not directory", resourceName));
}

if (resource.getAlias() != null) {
// this resource has an alias, use the alias, as that's what's returned in the Scanner
monitoredFile = new File(resource.getAlias());
}

file = monitoredFile;
if (LOG.isDebugEnabled()) {
LOG.debug("File monitoring started {} [{}]", resourceName, monitoredFile);
}
} catch (final IOException e) {
throw new IllegalArgumentException(String.format("could not obtain %s file", resourceName), e);
}

File parentFile = file.getParentFile();
if (!parentFile.exists() || !parentFile.isDirectory()) {
throw new IllegalArgumentException(String.format("error obtaining %s dir", resourceName));
}

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(final String filename) {
LOG.debug("Resource [{}] File [{}] added", resourceName, filename);
reloadMatched(filename);
}

@Override
public void fileChanged(final String filename) {
LOG.debug("Resource [{}] File [{}] changed", resourceName, filename);
reloadMatched(filename);
}

@Override
public void fileRemoved(final String filename) {
LOG.debug("Resource [{}] File [{}] removed", resourceName, filename);
reloadMatched(filename);
}

@ManagedOperation(
value = "Scan for changes in the SSL Keystore/Truststore",
impact = "ACTION"
)
public void scan() {
LOG.debug("Resource [{}] scanning started", resourceName);

this.scanner.scan();
this.scanner.scan();
}

@ManagedOperation(
value = "Reload the SSL Keystore/Truststore",
impact = "ACTION"
)
public void reload() {
LOG.debug("File [{}] reload started", file);

try {
this.sslContextFactory.reload(contextFactory -> contextFactory.setSslContext(createContext()));
LOG.info("File [{}] reload completed", file);
} catch (final Throwable t) {
LOG.warn("File [{}] reload failed", file, t);
}
}

@ManagedAttribute("scanning interval to detect changes which need reloaded")
public int getScanInterval() {
return this.scanner.getScanInterval();
}

public void setScanInterval(int scanInterval) {
this.scanner.setScanInterval(scanInterval);
}

private void reloadMatched(final String filename) {
if (file.toPath().toString().equals(filename)) {
reload();
}
}

private SSLContext createContext() {
try {
return createSslContext(tlsConfiguration);
} catch (final TlsException e) {
throw new IllegalArgumentException("Failed to create SSL context with the TLS configuration", e);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.server.util.TrustStoreScanner;
import org.apache.nifi.web.server.util.StoreScanner;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -182,11 +182,8 @@ private void assertCipherSuitesConfigured(final SslContextFactory sslContextFact

private void assertAutoReloadEnabled(final ServerConnector serverConnector) {
final Server server = serverConnector.getServer();
final KeyStoreScanner keyStoreScanner = server.getBean(KeyStoreScanner.class);
assertNotNull(keyStoreScanner);

final TrustStoreScanner trustStoreScanner = server.getBean(TrustStoreScanner.class);
assertNotNull(trustStoreScanner);
final Collection<StoreScanner> scanners = server.getBeans(StoreScanner.class);
assertEquals(2, scanners.size());
}

private NiFiProperties getProperties(final Properties serverProperties) {
Expand Down

0 comments on commit 3649ade

Please sign in to comment.