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