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

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


The following commit(s) were added to refs/heads/main by this push:
     new 0f4da77bbb37 CAMEL-25649: camel-netty - hostnameVerification verifies 
the host instead of failing every handshake (#27820)
0f4da77bbb37 is described below

commit 0f4da77bbb37464e4f77e09246ec092a70053d7a
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 14 21:19:28 2026 +0200

    CAMEL-25649: camel-netty - hostnameVerification verifies the host instead 
of failing every handshake (#27820)
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../apache/camel/catalog/docs/netty-component.adoc |  16 ++
 .../camel-netty/src/main/docs/netty-component.adoc |  16 ++
 .../netty/DefaultClientInitializerFactory.java     |  21 +-
 .../netty/DefaultServerInitializerFactory.java     |  27 ++-
 ...ConsumerClientModeHostnameVerificationTest.java | 214 +++++++++++++++++++++
 .../netty/NettySSLHostnameVerificationTest.java    | 117 +++++++++++
 .../src/test/resources/keystore-localhost.jks      | Bin 0 -> 3990 bytes
 .../src/test/resources/keystore-localhost.txt      |  26 +++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  20 ++
 9 files changed, 454 insertions(+), 3 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
index 02a42edec350..f33c4058a47d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
@@ -674,6 +674,22 @@ YAML::
 ----
 ====
 
+=== Verifying the host name of the server
+
+A producer, and a consumer with `clientMode=true`, connects as a TLS client 
when `ssl=true`. By default it accepts any
+certificate the JVM trusts, whatever host that certificate was issued for. Set 
`hostnameVerification=true` to also
+check that the certificate matches the host the endpoint connects to, with the 
same rules as an HTTPS client: a
+connection to an IP address needs a certificate that lists that address as a 
subject alternative name of its own.
+
+A certificate that does not match makes the producer or consumer fail with an
+`SSLHandshakeException: No subject alternative names matching IP address ... 
found` (or
+`No subject alternative DNS name matching ... found` for a host name). 
Enabling the option also makes the JDK send the
+host as an SNI server name.
+
+The option applies to the client side only. On a consumer that acts as a 
server it is ignored, because the JDK does not
+support host name verification on that side of a handshake: asked to do it 
anyway, it rejects the client certificate
+with `Endpoint Identification Algorithm HTTPS is not supported on the server 
side`.
+
 === An SSL/TCP-based Netty consumer endpoint using Request-Reply communication
 
 [[Netty-UsingtheJSSEConfigurationUtility]]
diff --git a/components/camel-netty/src/main/docs/netty-component.adoc 
b/components/camel-netty/src/main/docs/netty-component.adoc
index 02a42edec350..f33c4058a47d 100644
--- a/components/camel-netty/src/main/docs/netty-component.adoc
+++ b/components/camel-netty/src/main/docs/netty-component.adoc
@@ -674,6 +674,22 @@ YAML::
 ----
 ====
 
+=== Verifying the host name of the server
+
+A producer, and a consumer with `clientMode=true`, connects as a TLS client 
when `ssl=true`. By default it accepts any
+certificate the JVM trusts, whatever host that certificate was issued for. Set 
`hostnameVerification=true` to also
+check that the certificate matches the host the endpoint connects to, with the 
same rules as an HTTPS client: a
+connection to an IP address needs a certificate that lists that address as a 
subject alternative name of its own.
+
+A certificate that does not match makes the producer or consumer fail with an
+`SSLHandshakeException: No subject alternative names matching IP address ... 
found` (or
+`No subject alternative DNS name matching ... found` for a host name). 
Enabling the option also makes the JDK send the
+host as an SNI server name.
+
+The option applies to the client side only. On a consumer that acts as a 
server it is ignored, because the JDK does not
+support host name verification on that side of a handshake: asked to do it 
anyway, it rejects the client certificate
+with `Endpoint Identification Algorithm HTTPS is not supported on the server 
side`.
+
 === An SSL/TCP-based Netty consumer endpoint using Request-Reply communication
 
 [[Netty-UsingtheJSSEConfigurationUtility]]
diff --git 
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultClientInitializerFactory.java
 
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultClientInitializerFactory.java
index e47a8b1409ef..05a5c1db6294 100644
--- 
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultClientInitializerFactory.java
+++ 
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultClientInitializerFactory.java
@@ -127,6 +127,25 @@ public class DefaultClientInitializerFactory extends 
ClientInitializerFactory {
         return answer;
     }
 
+    /**
+     * Creates the client engine, carrying the host and port when host name 
verification is enabled.
+     * <p/>
+     * The JDK reads the name to match from {@code SSLSession.getPeerHost()}, 
which is only populated when the engine is
+     * created with the peer. With the no-arg factory method it stays {@code 
null} and {@code HostnameChecker.match}
+     * throws {@code CertificateException: Hostname or IP address is 
undefined} instead of checking anything, so every
+     * handshake fails. camel-netty-http creates its client engine the same 
way.
+     * <p/>
+     * The peer is attached only when the option is enabled, because it also 
makes the JDK send SNI - a change on the
+     * wire that the default path should not make.
+     */
+    private SSLEngine createClientSSLEngine() {
+        NettyConfiguration configuration = producer.getConfiguration();
+        if (configuration.isHostnameVerification()) {
+            return sslContext.createSSLEngine(configuration.getHost(), 
configuration.getPort());
+        }
+        return sslContext.createSSLEngine();
+    }
+
     private SslHandler configureClientSSLOnDemand() {
         if (!producer.getConfiguration().isSsl()) {
             return null;
@@ -135,7 +154,7 @@ public class DefaultClientInitializerFactory extends 
ClientInitializerFactory {
         if (producer.getConfiguration().getSslHandler() != null) {
             return producer.getConfiguration().getSslHandler();
         } else if (sslContext != null) {
-            SSLEngine engine = sslContext.createSSLEngine();
+            SSLEngine engine = createClientSSLEngine();
             engine.setUseClientMode(true);
             if (producer.getConfiguration().isHostnameVerification()) {
                 SSLParameters sslParams = engine.getSSLParameters();
diff --git 
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultServerInitializerFactory.java
 
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultServerInitializerFactory.java
index 7a95835b2370..12f992222084 100644
--- 
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultServerInitializerFactory.java
+++ 
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/DefaultServerInitializerFactory.java
@@ -130,6 +130,29 @@ public class DefaultServerInitializerFactory extends 
ServerInitializerFactory {
         return answer;
     }
 
+    /**
+     * Creates the engine, carrying the host and port when this consumer 
connects as a client with host name
+     * verification enabled.
+     * <p/>
+     * The JDK reads the name to match from {@code SSLSession.getPeerHost()}, 
which the no-arg factory method leaves
+     * {@code null} - and then {@code HostnameChecker.match} throws
+     * {@code CertificateException: Hostname or IP address is undefined} 
rather than checking anything. See
+     * {@code DefaultClientInitializerFactory} for the producer side.
+     * <p/>
+     * In server mode there is nothing to attach, and the algorithm is not set 
at all: the JDK does not support it on
+     * that side. When it checks a client certificate it skips the SNI name 
and uses {@code getPeerHost()}, and on a
+     * server engine it turns the resulting failure into
+     * {@code CertificateException: Endpoint Identification Algorithm HTTPS is 
not supported on the server side}. So it
+     * could only break a handshake in which the client presents a certificate 
- it never verified anything.
+     */
+    private SSLEngine createServerSSLEngine() {
+        NettyConfiguration configuration = consumer.getConfiguration();
+        if (configuration.isClientMode() && 
configuration.isHostnameVerification()) {
+            return sslContext.createSSLEngine(configuration.getHost(), 
configuration.getPort());
+        }
+        return sslContext.createSSLEngine();
+    }
+
     private SslHandler configureServerSSLOnDemand() {
         if (!consumer.getConfiguration().isSsl()) {
             return null;
@@ -138,14 +161,14 @@ public class DefaultServerInitializerFactory extends 
ServerInitializerFactory {
         if (consumer.getConfiguration().getSslHandler() != null) {
             return consumer.getConfiguration().getSslHandler();
         } else if (sslContext != null) {
-            SSLEngine engine = sslContext.createSSLEngine();
+            SSLEngine engine = createServerSSLEngine();
             
engine.setUseClientMode(consumer.getConfiguration().isClientMode());
             if (consumer.getConfiguration().isNeedClientAuth()) {
                 // only when enabled, to keep the client authentication from 
the sslContextParameters (such as
                 // REQUIRE or WANT) when needClientAuth is not enabled
                 engine.setNeedClientAuth(true);
             }
-            if (consumer.getConfiguration().isHostnameVerification()) {
+            if (consumer.getConfiguration().isClientMode() && 
consumer.getConfiguration().isHostnameVerification()) {
                 SSLParameters sslParams = engine.getSSLParameters();
                 sslParams.setEndpointIdentificationAlgorithm("HTTPS");
                 engine.setSSLParameters(sslParams);
diff --git 
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettySSLConsumerClientModeHostnameVerificationTest.java
 
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettySSLConsumerClientModeHostnameVerificationTest.java
new file mode 100644
index 000000000000..8e93d4a33b45
--- /dev/null
+++ 
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettySSLConsumerClientModeHostnameVerificationTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.camel.component.netty;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.DelimiterBasedFrameDecoder;
+import io.netty.handler.codec.Delimiters;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import io.netty.handler.ssl.SslHandler;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.parallel.Isolated;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A consumer with {@code clientMode=true} connects as a TCP client, so {@code 
hostnameVerification=true} applies to it
+ * as well and has to check the certificate of the server it dials.
+ * <p/>
+ * The server below presents keystore-localhost.jks, whose only subject 
alternative name is DNS:localhost, and the
+ * consumer connects to localhost - so the check passes and the message is 
routed. Without the peer on the engine the
+ * JDK cannot run the check and fails the handshake instead, so nothing would 
reach the mock.
+ */
+@Isolated("Does not seem to run reliably in parallel")
+@DisabledIfSystemProperty(named = "java.vendor", matches = ".*ibm.*")
+class NettySSLConsumerClientModeHostnameVerificationTest extends BaseNettyTest 
{
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(NettySSLConsumerClientModeHostnameVerificationTest.class);
+    private static final String KEYSTORE = 
"src/test/resources/keystore-localhost.jks";
+    private static final char[] PASSWORD = "changeit".toCharArray();
+
+    private MyServer server;
+
+    @BindToRegistry("ksf")
+    public File loadKeystoreKsf() {
+        return new File(KEYSTORE);
+    }
+
+    @BindToRegistry("tsf")
+    public File loadKeystoreTsf() {
+        return new File(KEYSTORE);
+    }
+
+    @Test
+    void verifiesTheHostOfTheServerItConnectsTo() throws Exception {
+        server = new MyServer(getPort());
+        try {
+            server.start();
+            MockEndpoint receive = context.getEndpoint("mock:receive", 
MockEndpoint.class);
+            receive.expectedBodiesReceived("Bye Willem");
+            context.getRouteController().startRoute("sslclient");
+            receive.assertIsSatisfied();
+        } finally {
+            server.shutdown();
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("netty:tcp://localhost:{{port}}?textline=true&clientMode=true&ssl=true&hostnameVerification=true"
+                     + 
"&passphrase=changeit&keyStoreResource=#ksf&trustStoreResource=#tsf")
+                        .id("sslclient")
+                        .setBody(simple("Bye ${body}"))
+                        .to("mock:receive")
+                        .autoStartup(false);
+            }
+        };
+    }
+
+    private static SSLContext serverSslContext() throws Exception {
+        KeyStore ks = KeyStore.getInstance("JKS");
+        try (InputStream in = new FileInputStream(new File(KEYSTORE))) {
+            ks.load(in, PASSWORD);
+        }
+        KeyManagerFactory kmf = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        kmf.init(ks, PASSWORD);
+        TrustManagerFactory tmf = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        tmf.init(ks);
+
+        SSLContext answer = SSLContext.getInstance("TLS");
+        answer.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+        return answer;
+    }
+
+    /** Greets whoever connects, so the consumer has something to route. */
+    private static final class MyServer {
+
+        private final int port;
+        private Channel channel;
+        private EventLoopGroup bossGroup;
+        private EventLoopGroup workerGroup;
+
+        MyServer(int port) {
+            this.port = port;
+        }
+
+        void start() throws Exception {
+            bossGroup = new NioEventLoopGroup(1);
+            workerGroup = new NioEventLoopGroup();
+
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new ServerInitializer());
+
+            ChannelFuture cf = bootstrap.bind(port).sync();
+            channel = cf.channel();
+        }
+
+        void shutdown() {
+            if (channel != null) {
+                channel.disconnect();
+            }
+            if (bossGroup != null) {
+                bossGroup.shutdownGracefully();
+            }
+            if (workerGroup != null) {
+                workerGroup.shutdownGracefully();
+            }
+        }
+    }
+
+    private static final class ServerHandler extends 
SimpleChannelInboundHandler<String> {
+
+        @Override
+        public void channelActive(ChannelHandlerContext ctx) {
+            ctx.write("Willem\r\n");
+            ctx.flush();
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable 
cause) {
+            LOG.warn("Unhandled exception caught: {}", cause.getMessage(), 
cause);
+            ctx.close();
+        }
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
+            // the consumer is the one that routes, the server only greets
+        }
+
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx) {
+            ctx.flush();
+        }
+    }
+
+    private static final class ServerInitializer extends 
ChannelInitializer<SocketChannel> {
+
+        private final SSLContext sslContext;
+
+        ServerInitializer() {
+            try {
+                sslContext = serverSslContext();
+            } catch (Exception e) {
+                throw new IllegalStateException("Could not create the server 
SSLContext", e);
+            }
+        }
+
+        @Override
+        public void initChannel(SocketChannel ch) {
+            SSLEngine engine = sslContext.createSSLEngine();
+            engine.setUseClientMode(false);
+
+            ChannelPipeline pipeline = ch.pipeline();
+            pipeline.addLast("ssl", new SslHandler(engine));
+            pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, 
Delimiters.lineDelimiter()));
+            pipeline.addLast("decoder", new StringDecoder());
+            pipeline.addLast("encoder", new StringEncoder());
+            pipeline.addLast("handler", new ServerHandler());
+        }
+    }
+}
diff --git 
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettySSLHostnameVerificationTest.java
 
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettySSLHostnameVerificationTest.java
new file mode 100644
index 000000000000..c43cad90c345
--- /dev/null
+++ 
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettySSLHostnameVerificationTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.camel.component.netty;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.jsse.KeyManagersParameters;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The producer checks that the certificate of the server matches the host it 
connects to when
+ * {@code hostnameVerification=true}.
+ * <p/>
+ * keystore-localhost.jks lists only DNS:localhost as subject alternative 
name, so it matches "localhost" but not
+ * "127.0.0.1" - which both routes below resolve to, so the same consumer 
serves them.
+ */
+@DisabledIfSystemProperty(named = "java.vendor", matches = ".*ibm.*")
+class NettySSLHostnameVerificationTest extends BaseNettyTest {
+
+    private static final String REPLY = "Bonjour";
+
+    @BindToRegistry("sslContextParameters")
+    public SSLContextParameters loadSSLContextParams() {
+        KeyStoreParameters ksp = new KeyStoreParameters();
+        
ksp.setResource(this.getClass().getClassLoader().getResource("keystore-localhost.jks").toString());
+        ksp.setPassword("changeit");
+
+        KeyManagersParameters kmp = new KeyManagersParameters();
+        kmp.setKeyPassword("changeit");
+        kmp.setKeyStore(ksp);
+
+        TrustManagersParameters tmp = new TrustManagersParameters();
+        tmp.setKeyStore(ksp);
+
+        SSLContextParameters sslContextParameters = new SSLContextParameters();
+        sslContextParameters.setKeyManagers(kmp);
+        sslContextParameters.setTrustManagers(tmp);
+        return sslContextParameters;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                
from("netty:tcp://localhost:{{port}}?sync=true&ssl=true&sslContextParameters=#sslContextParameters")
+                        .setBody(constant(REPLY));
+            }
+        };
+    }
+
+    @Test
+    void connectsWhenTheCertificateMatchesTheHost() {
+        String out = template.requestBody(
+                
"netty:tcp://localhost:{{port}}?sync=true&ssl=true&sslContextParameters=#sslContextParameters"
+                                          + "&hostnameVerification=true",
+                "Hello", String.class);
+
+        assertEquals(REPLY, out);
+    }
+
+    /**
+     * Without a peer host on the engine the JDK cannot run the check at all 
and reports "Hostname or IP address is
+     * undefined", which would make this pass for the wrong reason - so the 
message is asserted as well.
+     */
+    @Test
+    void failsWhenTheCertificateDoesNotMatchTheHost() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.requestBody(
+                        
"netty:tcp://127.0.0.1:{{port}}?sync=true&ssl=true&sslContextParameters=#sslContextParameters"
+                                           + "&hostnameVerification=true",
+                        "Hello", String.class));
+
+        String message = rootCause(e).getMessage();
+        assertTrue(message != null && message.contains("127.0.0.1"),
+                "the host name check should have rejected the certificate, but 
got: " + message);
+    }
+
+    @Test
+    void 
connectsToAHostTheCertificateDoesNotListWhenHostnameVerificationIsDisabled() {
+        String out = template.requestBody(
+                
"netty:tcp://127.0.0.1:{{port}}?sync=true&ssl=true&sslContextParameters=#sslContextParameters",
+                "Hello", String.class);
+
+        assertEquals(REPLY, out);
+    }
+
+    private static Throwable rootCause(Throwable t) {
+        Throwable answer = t;
+        while (answer.getCause() != null) {
+            answer = answer.getCause();
+        }
+        return answer;
+    }
+}
diff --git a/components/camel-netty/src/test/resources/keystore-localhost.jks 
b/components/camel-netty/src/test/resources/keystore-localhost.jks
new file mode 100644
index 000000000000..23efaf153d9c
Binary files /dev/null and 
b/components/camel-netty/src/test/resources/keystore-localhost.jks differ
diff --git a/components/camel-netty/src/test/resources/keystore-localhost.txt 
b/components/camel-netty/src/test/resources/keystore-localhost.txt
new file mode 100644
index 000000000000..18a9d9bf78cd
--- /dev/null
+++ b/components/camel-netty/src/test/resources/keystore-localhost.txt
@@ -0,0 +1,26 @@
+src/test/resources/keystore-localhost.jks holds a certificate for "localhost", 
listed as a subject
+alternative name, so it can be used to test host name verification. It was 
created using the
+following commands:
+openssl req -x509 \
+ -newkey rsa:4096 \
+ -keyout key.pem \
+ -out cert.pem \
+ -days 3650 \
+ -sha256 \
+ -nodes \
+ -subj 
"/C=US/ST=State/L=Loc/O=Org/OU=Unit/CN=localhost/serialNumber=1234567891" \
+ -set_serial 1234567891 \
+ -addext "subjectAltName=DNS:localhost"
+openssl pkcs12 -export \
+ -out keystore.p12 \
+ -inkey key.pem \
+ -in cert.pem \
+ -name cert \
+ -passout pass:changeit
+keytool -importkeystore \
+ -srckeystore keystore.p12 \
+ -srcstoretype PKCS12 \
+ -destkeystore keystore-localhost.jks \
+ -deststoretype JKS \
+ -srcstorepass changeit \
+ -deststorepass changeit
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 58a0c714a4cd..7876caf40a53 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -452,6 +452,26 @@ threw an `IndexOutOfBoundsException` at the end. 
`markSupported()` now returns `
 anything): a reader that needs mark support has to wrap the stream, for 
example in a `BufferedInputStream`.
 `reset()` goes back to the start of the body, as before.
 
+=== camel-netty - hostnameVerification verifies the host instead of failing
+
+`hostnameVerification=true` on a `netty` producer, or on a consumer with 
`clientMode=true`, now checks that the
+certificate of the server matches the host the endpoint connects to. Before, 
it made the TLS handshake fail for every
+certificate, matching or not, with `CertificateException: Hostname or IP 
address is undefined` - the engine was
+created without a peer host, so the JDK had no name to compare against.
+
+A route that had the option set was therefore not connecting at all, and needs 
no change. A route that sets it while
+connecting to a host the certificate does not list - most commonly an IP 
address, which has to appear as a subject
+alternative name of its own - now fails the handshake with the mismatch as the 
reason. The default is unchanged:
+`hostnameVerification` is `false`, and the host is then not checked.
+
+Enabling the option also makes the JDK send the host as an SNI server name, as 
`camel-netty-http` already did. The
+default path still creates the engine without a peer and sends no SNI.
+
+On a consumer that acts as a server the option is now ignored instead of being 
passed to the engine. It never verified
+anything there - the JDK does not support host name verification on the server 
side of a handshake, and rejected the
+client certificate with `Endpoint Identification Algorithm HTTPS is not 
supported on the server side` - so a consumer
+that asks for client certificates and had the option set now completes the 
handshake instead of failing it.
+
 === camel-dropbox - search and get read every page of the result
 
 The `search` operation (producer and consumer) now returns every match that 
the service gives. Previously it returned

Reply via email to