This is an automated email from the ASF dual-hosted git repository.
pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 2e700201eb6 NIFI-16184 Add TLS Endpoint Identification for Cluster
Communication (#11530)
2e700201eb6 is described below
commit 2e700201eb6fd16831be37689090976529634a6d
Author: David Handermann <[email protected]>
AuthorDate: Wed Aug 12 14:50:24 2026 -0500
NIFI-16184 Add TLS Endpoint Identification for Cluster Communication
(#11530)
---
.../src/main/asciidoc/administration-guide.adoc | 10 +-
.../nifi-framework-cluster-protocol/pom.xml | 22 ++-
.../org/apache/nifi/io/socket/SocketUtils.java | 7 +
.../org/apache/nifi/io/socket/SocketUtilsTest.java | 155 +++++++++++++++++++++
.../nifi-framework/nifi-framework-core/pom.xml | 5 +
.../async/nio/NioAsyncLoadBalanceClient.java | 23 +--
.../clustered/client/async/nio/PeerChannel.java | 4 +
.../client/async/nio/PeerChannelProvider.java | 33 +++++
.../async/nio/StandardPeerChannelProvider.java | 79 +++++++++++
.../async/nio/TestStandardPeerChannelProvider.java | 123 ++++++++++++++++
.../tests/system/loadbalance/LoadBalanceIT.java | 2 +-
11 files changed, 435 insertions(+), 28 deletions(-)
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 138a50631f8..5ba8082df22 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -3683,9 +3683,13 @@ by the `nifi.cluster.flow.election.max.candidates`
property, the cluster will no
|`nifi.cluster.flow.election.max.candidates`|Specifies the number of Nodes
required in the cluster to cause early election of Flows. This allows the Nodes
in the cluster to avoid having to wait a
long time before starting processing if we reach at least this number of nodes
in the cluster.
|`nifi.cluster.load.balance.port`|Specifies the port to listen on for incoming
connections for load balancing data across the cluster. The default value is
`6342`.
-|`nifi.cluster.load.balance.host`|Specifies the hostname to listen on for
incoming connections for load balancing data across the cluster. If not
specified, will default to the value used by the
-`nifi.cluster.node.address` property. The value set here does not have to be a
hostname/IP address that is addressable outside of the cluster. However, all
nodes within the cluster must be able to
-connect to the node using this hostname/IP address.
+|`nifi.cluster.load.balance.host`|Specifies the hostname to listen on for
incoming connections for load balancing data
+across the cluster. If not specified, will default to the value used by the
`nifi.cluster.node.address` property. The
+value set here does not have to be a hostname address that is addressable
outside of the cluster. However, all nodes
+within the cluster must be able to connect to the node using this hostname
address.
+
+When TLS encryption is enabled for cluster communication, this value must be
set to a hostname that aligns with DNS
+Subject Alternative Names included on the server certificate configured in the
application key store.
|`nifi.cluster.load.balance.connections.per.node`|The maximum number of
connections to create between this node and each other node in the cluster. For
example, if there are 5 nodes in the cluster and this value is set to 4, there
will be up to 20 socket connections established for load-balancing purposes (5
x 4 = 20). The default value is `1`.
|`nifi.cluster.load.balance.max.thread.count`|The maximum number of threads to
use for transferring data from this node to other nodes in the cluster. While a
given thread can only write to a single socket at a time, a single thread is
capable of servicing multiple connections simultaneously because a given
connection may not be available for reading/writing at any given time. The
default value is `8`—i.e., up to 8 threads will be responsible for transferring
data to other nodes, regardl [...]
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml
index dee8a3e1f12..6a8c7cdd50d 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml
@@ -70,11 +70,6 @@
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
- <dependency>
- <groupId>org.glassfish.jaxb</groupId>
- <artifactId>jaxb-runtime</artifactId>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
@@ -87,5 +82,22 @@
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.glassfish.jaxb</groupId>
+ <artifactId>jaxb-runtime</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-security-ssl</artifactId>
+ <version>2.12.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-security-cert-builder</artifactId>
+ <version>2.12.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/io/socket/SocketUtils.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/io/socket/SocketUtils.java
index eb126a26bab..05ca248e194 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/io/socket/SocketUtils.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/io/socket/SocketUtils.java
@@ -28,6 +28,7 @@ import java.net.Socket;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
@@ -35,6 +36,8 @@ public final class SocketUtils {
private static final Logger logger =
LoggerFactory.getLogger(SocketUtils.class);
+ private static final String TLS_ENDPOINT_IDENTIFICATION_ALGORITHM =
"HTTPS";
+
/**
* Returns a {@link Socket} (effectively used as a client socket) for the
given address and configuration.
*
@@ -63,6 +66,10 @@ public final class SocketUtils {
final SSLSocket sslSocket = (SSLSocket) tempSocket;
// Set Preferred TLS Protocol Versions
sslSocket.setEnabledProtocols(TlsPlatform.getPreferredProtocols().toArray(new
String[0]));
+ final SSLParameters sslParameters = sslSocket.getSSLParameters();
+
sslParameters.setEndpointIdentificationAlgorithm(TLS_ENDPOINT_IDENTIFICATION_ALGORITHM);
+ sslSocket.setSSLParameters(sslParameters);
+
socket = sslSocket;
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/test/java/org/apache/nifi/io/socket/SocketUtilsTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/test/java/org/apache/nifi/io/socket/SocketUtilsTest.java
new file mode 100644
index 00000000000..c964bfebde2
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/test/java/org/apache/nifi/io/socket/SocketUtilsTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.io.socket;
+
+import org.apache.nifi.security.cert.builder.StandardCertificateBuilder;
+import org.apache.nifi.security.ssl.EphemeralKeyStoreBuilder;
+import org.apache.nifi.security.ssl.StandardSslContextBuilder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocket;
+import javax.security.auth.x500.X500Principal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SocketUtilsTest {
+
+ private static final char[] EMPTY_PASSWORD = new char[]{};
+
+ private static final Duration CERTIFICATE_VALIDITY = Duration.ofHours(1);
+
+ private static final int TIMEOUT_MILLISECONDS = 5000;
+
+ private static final String LOCALHOST = "localhost";
+
+ private static final String NON_MATCHING_HOST = "nifi.apache.org";
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SUBJECT_FORMAT = "CN=%s";
+
+ private ExecutorService executorService;
+
+ @BeforeEach
+ void setExecutorService() {
+ executorService = Executors.newSingleThreadExecutor();
+ }
+
+ @AfterEach
+ void shutdownExecutorService() {
+ executorService.shutdownNow();
+ }
+
+ @Test
+ void testCreateSocketVerifiesMatchingHostname() throws Exception {
+ final KeyPair keyPair = generateKeyPair();
+ final X509Certificate certificate = buildCertificate(keyPair,
LOCALHOST);
+
+ connectAndHandshake(keyPair, certificate);
+ }
+
+ @Test
+ void testCreateSocketRejectsMismatchedHostname() throws Exception {
+ final KeyPair keyPair = generateKeyPair();
+ final X509Certificate certificate = buildCertificate(keyPair,
NON_MATCHING_HOST);
+
+ assertThrows(SSLHandshakeException.class, () ->
connectAndHandshake(keyPair, certificate));
+ }
+
+ private void connectAndHandshake(final KeyPair serverKeyPair, final
X509Certificate serverCertificate) throws Exception {
+ final SSLContext serverSslContext =
buildServerSslContext(serverKeyPair, serverCertificate);
+ final SSLContext clientSslContext =
buildClientSslContext(serverCertificate);
+
+ final ServerSocketConfiguration serverConfiguration = new
ServerSocketConfiguration();
+ serverConfiguration.setSslContext(serverSslContext);
+ serverConfiguration.setSocketTimeout(TIMEOUT_MILLISECONDS);
+
+ try (ServerSocket serverSocket = SocketUtils.createServerSocket(0,
serverConfiguration)) {
+ final int port = serverSocket.getLocalPort();
+
+ final Future<?> serverConnection = executorService.submit(() -> {
+ try (Socket accepted = serverSocket.accept()) {
+ // Read to initiate handshaking
+ final int read = accepted.getInputStream().read();
+ assertEquals(0, read);
+ }
+ return null;
+ });
+
+ final SocketConfiguration clientConfiguration = new
SocketConfiguration();
+ clientConfiguration.setSslContext(clientSslContext);
+ clientConfiguration.setSocketTimeout(TIMEOUT_MILLISECONDS);
+
+ try (Socket socket = SocketUtils.createSocket(new
InetSocketAddress(LOCALHOST, port), clientConfiguration)) {
+ final SSLSocket sslSocket = (SSLSocket) socket;
+ sslSocket.startHandshake();
+ sslSocket.getOutputStream().write(0);
+ assertTrue(sslSocket.getSession().isValid());
+ } finally {
+ serverConnection.cancel(true);
+ }
+ }
+ }
+
+ private SSLContext buildServerSslContext(final KeyPair keyPair, final
X509Certificate certificate) {
+ final KeyStore keyStore = new EphemeralKeyStoreBuilder()
+ .addPrivateKeyEntry(new
KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{certificate}))
+ .build();
+ return new StandardSslContextBuilder()
+ .keyStore(keyStore)
+ .keyPassword(EMPTY_PASSWORD)
+ .build();
+ }
+
+ private SSLContext buildClientSslContext(final X509Certificate
certificate) {
+ final KeyStore trustStore = new EphemeralKeyStoreBuilder()
+ .addCertificate(certificate)
+ .build();
+ return new StandardSslContextBuilder()
+ .trustStore(trustStore)
+ .build();
+ }
+
+ private X509Certificate buildCertificate(final KeyPair keyPair, final
String commonName) {
+ final X500Principal subject = new
X500Principal(SUBJECT_FORMAT.formatted(commonName));
+ return new StandardCertificateBuilder(keyPair, subject,
CERTIFICATE_VALIDITY).build();
+ }
+
+ private KeyPair generateKeyPair() throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyPairGenerator =
KeyPairGenerator.getInstance(KEY_ALGORITHM);
+ return keyPairGenerator.generateKeyPair();
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
index 3cc4033fc93..6b7665cb01d 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
@@ -257,6 +257,11 @@
<artifactId>nifi-authorizer</artifactId>
<version>2.12.0-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-security-ssl</artifactId>
+ <version>2.12.0-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock-authorizer</artifactId>
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java
index 376dcac1e34..e7b8834a30f 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java
@@ -56,19 +56,18 @@ import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
public class NioAsyncLoadBalanceClient implements AsyncLoadBalanceClient {
private static final Logger logger =
LoggerFactory.getLogger(NioAsyncLoadBalanceClient.class);
private static final long PENALIZATION_MILLIS =
TimeUnit.SECONDS.toMillis(1L);
private final NodeIdentifier nodeIdentifier;
- private final SSLContext sslContext;
private final int timeoutMillis;
private final FlowFileContentAccess flowFileContentAccess;
private final LoadBalanceFlowFileCodec flowFileCodec;
private final EventReporter eventReporter;
private final ClusterCoordinator clusterCoordinator;
+ private final PeerChannelProvider peerChannelProvider;
private volatile boolean running = false;
private final AtomicLong penalizationEnd = new AtomicLong(0L);
@@ -92,12 +91,12 @@ public class NioAsyncLoadBalanceClient implements
AsyncLoadBalanceClient {
public NioAsyncLoadBalanceClient(final NodeIdentifier nodeIdentifier,
final SSLContext sslContext, final int timeoutMillis, final
FlowFileContentAccess flowFileContentAccess,
final LoadBalanceFlowFileCodec
flowFileCodec, final EventReporter eventReporter, final ClusterCoordinator
clusterCoordinator) {
this.nodeIdentifier = nodeIdentifier;
- this.sslContext = sslContext;
this.timeoutMillis = timeoutMillis;
this.flowFileContentAccess = flowFileContentAccess;
this.flowFileCodec = flowFileCodec;
this.eventReporter = eventReporter;
this.clusterCoordinator = clusterCoordinator;
+ this.peerChannelProvider = new StandardPeerChannelProvider(sslContext,
nodeIdentifier);
}
@Override
@@ -449,7 +448,8 @@ public class NioAsyncLoadBalanceClient implements
AsyncLoadBalanceClient {
socketChannel = createChannel();
socketChannel.configureBlocking(true);
- peerChannel = createPeerChannel(socketChannel,
socketChannel.getLocalAddress() + "::" + socketChannel.getRemoteAddress());
+ final String peerDescription = socketChannel.getLocalAddress()
+ "::" + socketChannel.getRemoteAddress();
+ peerChannel =
peerChannelProvider.getPeerChannel(socketChannel, peerDescription);
channel = peerChannel;
}
@@ -479,21 +479,6 @@ public class NioAsyncLoadBalanceClient implements
AsyncLoadBalanceClient {
}
}
- private PeerChannel createPeerChannel(final SocketChannel channel, final
String peerDescription) {
- if (sslContext == null) {
- logger.debug("No SSL Context is available so will not perform SSL
Handshake with Peer {}", peerDescription);
- return new PeerChannel(channel, null, peerDescription);
- }
-
- logger.debug("Performing SSL Handshake with Peer {}", peerDescription);
-
- final SSLEngine sslEngine = sslContext.createSSLEngine();
- sslEngine.setUseClientMode(true);
- sslEngine.setNeedClientAuth(true);
-
- return new PeerChannel(channel, sslEngine, peerDescription);
- }
-
private SocketChannel createChannel() throws IOException {
final SocketChannel socketChannel = SocketChannel.open();
try {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannel.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannel.java
index 894559b92a4..9ba759e16aa 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannel.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannel.java
@@ -105,6 +105,10 @@ public class PeerChannel implements Closeable {
return peerDescription;
}
+ SSLEngine getSslEngine() {
+ return sslEngine;
+ }
+
/**
* Write one byte to the channel
*
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannelProvider.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannelProvider.java
new file mode 100644
index 00000000000..8ad36914140
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/PeerChannelProvider.java
@@ -0,0 +1,33 @@
+/*
+ * 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.controller.queue.clustered.client.async.nio;
+
+import java.nio.channels.SocketChannel;
+
+/**
+ * Provider abstraction for creating a {@link PeerChannel} bound to a
connected Socket Channel for load balancing communication.
+ */
+interface PeerChannelProvider {
+ /**
+ * Get a Peer Channel for the provided Socket Channel with TLS when
supported
+ *
+ * @param socketChannel Connected Socket Channel for communication with
the peer
+ * @param peerDescription Description of the peer used for logging
+ * @return Peer Channel wrapping the provided Socket Channel
+ */
+ PeerChannel getPeerChannel(SocketChannel socketChannel, String
peerDescription);
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/StandardPeerChannelProvider.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/StandardPeerChannelProvider.java
new file mode 100644
index 00000000000..a414a673203
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/StandardPeerChannelProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.controller.queue.clustered.client.async.nio;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.channels.SocketChannel;
+import java.util.Objects;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+
+/**
+ * Standard implementation of Peer Channel Provider that configures TLS with
HTTPS endpoint identification so that
+ * the peer certificate is verified against the Load Balance address of the
destination node during the handshake.
+ */
+class StandardPeerChannelProvider implements PeerChannelProvider {
+
+ private static final Logger logger =
LoggerFactory.getLogger(StandardPeerChannelProvider.class);
+
+ private static final String TLS_ENDPOINT_IDENTIFICATION_ALGORITHM =
"HTTPS";
+
+ private final SSLContext sslContext;
+
+ private final NodeIdentifier nodeIdentifier;
+
+ StandardPeerChannelProvider(final SSLContext sslContext, final
NodeIdentifier nodeIdentifier) {
+ this.sslContext = sslContext;
+ this.nodeIdentifier = nodeIdentifier;
+ }
+
+ @Override
+ public PeerChannel getPeerChannel(final SocketChannel socketChannel, final
String peerDescription) {
+ Objects.requireNonNull(socketChannel, "Socket Channel required");
+ Objects.requireNonNull(peerDescription, "Peer Description required");
+
+ final PeerChannel peerChannel;
+
+ if (sslContext == null) {
+ logger.debug("SSLContext not configured for Peer Channel [{}]",
peerDescription);
+ peerChannel = new PeerChannel(socketChannel, null,
peerDescription);
+ } else {
+ logger.debug("Configured TLS for Peer Channel [{}]",
peerDescription);
+ final SSLEngine sslEngine = createSslEngine();
+ peerChannel = new PeerChannel(socketChannel, sslEngine,
peerDescription);
+ }
+
+ return peerChannel;
+ }
+
+ private SSLEngine createSslEngine() {
+ // Provide the peer address so endpoint identification can verify the
peer certificate against the Load Balance host
+ final SSLEngine sslEngine =
sslContext.createSSLEngine(nodeIdentifier.getLoadBalanceAddress(),
nodeIdentifier.getLoadBalancePort());
+ sslEngine.setUseClientMode(true);
+
+ final SSLParameters sslParameters = sslEngine.getSSLParameters();
+
sslParameters.setEndpointIdentificationAlgorithm(TLS_ENDPOINT_IDENTIFICATION_ALGORITHM);
+ sslEngine.setSSLParameters(sslParameters);
+
+ return sslEngine;
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/queue/clustered/client/async/nio/TestStandardPeerChannelProvider.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/queue/clustered/client/async/nio/TestStandardPeerChannelProvider.java
new file mode 100644
index 00000000000..d8ab874efcb
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/queue/clustered/client/async/nio/TestStandardPeerChannelProvider.java
@@ -0,0 +1,123 @@
+/*
+ * 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.controller.queue.clustered.client.async.nio;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.security.cert.builder.StandardCertificateBuilder;
+import org.apache.nifi.security.ssl.EphemeralKeyStoreBuilder;
+import org.apache.nifi.security.ssl.StandardSslContextBuilder;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.nio.channels.SocketChannel;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.security.auth.x500.X500Principal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+class TestStandardPeerChannelProvider {
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SUBJECT_FORMAT = "CN=%s";
+
+ private static final String LOAD_BALANCE_ADDRESS =
"node-1.nifi.example.com";
+
+ private static final int LOAD_BALANCE_PORT = 6342;
+
+ private static final String NODE_ID = "node-1";
+
+ private static final String LOCALHOST = "localhost";
+
+ private static final int NODE_PORT = 8443;
+
+ private static final String PEER_DESCRIPTION = "local::remote";
+
+ private static final String TLS_ENDPOINT_IDENTIFICATION_ALGORITHM =
"HTTPS";
+
+ private static final Duration CERTIFICATE_VALIDITY = Duration.ofHours(1);
+
+ private static final char[] EMPTY_PASSWORD = new char[]{};
+
+ private static SSLContext sslContext;
+
+ private static NodeIdentifier nodeIdentifier;
+
+ @BeforeAll
+ static void setConfiguration() throws GeneralSecurityException {
+ final KeyPair keyPair =
KeyPairGenerator.getInstance(KEY_ALGORITHM).generateKeyPair();
+ final X509Certificate certificate = new
StandardCertificateBuilder(keyPair, new
X500Principal(SUBJECT_FORMAT.formatted(LOAD_BALANCE_ADDRESS)),
CERTIFICATE_VALIDITY).build();
+ final KeyStore keyStore = new EphemeralKeyStoreBuilder()
+ .addPrivateKeyEntry(new
KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{certificate}))
+ .build();
+
+ sslContext = new StandardSslContextBuilder()
+ .trustStore(keyStore)
+ .keyStore(keyStore)
+ .keyPassword(EMPTY_PASSWORD)
+ .build();
+
+ nodeIdentifier = new NodeIdentifier(
+ NODE_ID,
+ LOCALHOST,
+ NODE_PORT,
+ LOCALHOST,
+ NODE_PORT,
+ LOAD_BALANCE_ADDRESS,
+ LOAD_BALANCE_PORT,
+ LOCALHOST,
+ NODE_PORT,
+ NODE_PORT,
+ false
+ );
+ }
+
+ @Test
+ void testGetPeerChannelConfiguresEndpointIdentification() {
+ final StandardPeerChannelProvider provider = new
StandardPeerChannelProvider(sslContext, nodeIdentifier);
+
+ final PeerChannel peerChannel =
provider.getPeerChannel(mock(SocketChannel.class), PEER_DESCRIPTION);
+ final SSLEngine sslEngine = peerChannel.getSslEngine();
+ assertNotNull(sslEngine, "SSLEngine not configured");
+
+ assertTrue(sslEngine.getUseClientMode(), "Client mode not enabled");
+ assertEquals(TLS_ENDPOINT_IDENTIFICATION_ALGORITHM,
sslEngine.getSSLParameters().getEndpointIdentificationAlgorithm());
+ assertEquals(LOAD_BALANCE_ADDRESS, sslEngine.getPeerHost(), "Peer host
not matched to Load Balance address");
+ assertEquals(LOAD_BALANCE_PORT, sslEngine.getPeerPort(), "Peer port
not matched to Load Balance port");
+ }
+
+ @Test
+ void testGetPeerChannelWithoutSslContext() {
+ final StandardPeerChannelProvider provider = new
StandardPeerChannelProvider(null, nodeIdentifier);
+
+ final PeerChannel peerChannel =
provider.getPeerChannel(mock(SocketChannel.class), PEER_DESCRIPTION);
+ assertNull(peerChannel.getSslEngine(), "SSLEngine should not be
configured without an SSL Context");
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/loadbalance/LoadBalanceIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/loadbalance/LoadBalanceIT.java
index 3a07c105722..4c07c4fece5 100644
---
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/loadbalance/LoadBalanceIT.java
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/loadbalance/LoadBalanceIT.java
@@ -354,7 +354,7 @@ public class LoadBalanceIT extends NiFiSystemIT {
instance2.stop();
final Map<String, String> updatedLoadBalanceProperties = new
HashMap<>();
- updatedLoadBalanceProperties.put("nifi.cluster.load.balance.host",
"127.0.0.1");
+ updatedLoadBalanceProperties.put("nifi.cluster.load.balance.host",
"localhost");
updatedLoadBalanceProperties.put("nifi.cluster.load.balance.port",
"7676");
instance2.setProperties(updatedLoadBalanceProperties);