diff --git a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/StrictKexInteroperabilityTest.java b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/StrictKexInteroperabilityTest.java
new file mode 100644
index 000000000..43e6d8a8e
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/StrictKexInteroperabilityTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.sshd.common.kex.extension;
+
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ChannelShell;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.session.ClientSessionImpl;
+import org.apache.sshd.client.session.SessionFactory;
+import org.apache.sshd.common.channel.StreamingChannel;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.apache.sshd.util.test.ContainerTestCase;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Tests to ensure that an Apache MINA sshd client can talk to OpenSSH servers with or without "strict KEX". This
+ * implicitly tests the message sequence number handling; if sequence numbers get out of sync or are reset wrongly,
+ * subsequent messages cannot be decrypted correctly and there will be exceptions.
+ *
+ * @author Apache MINA SSHD Project
+ * @see Terrapin Mitigation: "strict-kex"
+ */
+@Category(ContainerTestCase.class)
+public class StrictKexInteroperabilityTest extends BaseTestSupport {
+
+ private static final Logger LOG = LoggerFactory.getLogger(StrictKexInteroperabilityTest.class);
+
+ private static final String TEST_RESOURCES = "org/apache/sshd/common/kex/extensions/client";
+
+ private SshClient client;
+
+ public StrictKexInteroperabilityTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ client = setupTestClient();
+ SessionFactory factory = new TestSessionFactory(client);
+ client.setSessionFactory(factory);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (client != null) {
+ client.stop();
+ }
+ }
+
+ private DockerfileBuilder strictKexImage(DockerfileBuilder builder, boolean withStrictKex) {
+ if (!withStrictKex) {
+ return builder
+ // CentOS 7 is EOL and thus unlikely to get the security update for strict KEX.
+ .from("centos:7.9.2009") //
+ .run("yum install -y openssh-server") // Installs OpenSSH 7.4
+ .run("/usr/sbin/sshd-keygen") // Generate multiple host keys
+ .run("adduser bob"); // Add a user
+ } else {
+ return builder
+ .from("alpine:20231219") //
+ .run("apk --update add openssh-server") // Installs OpenSSH 9.6
+ .run("ssh-keygen -A") // Generate multiple host keys
+ .run("adduser -D bob") // Add a user
+ .run("echo 'bob:passwordBob' | chpasswd"); // Give it a password to unlock the user
+ }
+ }
+
+ @Test
+ public void testStrictKexOff() throws Exception {
+ testStrictKex(false);
+ }
+
+ @Test
+ public void testStrictKexOn() throws Exception {
+ testStrictKex(true);
+ }
+
+ private void testStrictKex(boolean withStrictKex) throws Exception {
+ // This tests that the message sequence numbers are handled correctly. Strict KEX resets them to zero on any
+ // KEX, without strict KEX, they're not reset. If sequence numbers get out of sync, received messages are
+ // decrypted wrongly and there will be exceptions.
+ @SuppressWarnings("resource")
+ GenericContainer> sshdContainer = new GenericContainer<>(new ImageFromDockerfile()
+ .withDockerfileFromBuilder(builder -> strictKexImage(builder, withStrictKex) //
+ .run("mkdir -p /home/bob/.ssh") // Create the SSH config directory
+ .entryPoint("/entrypoint.sh") //
+ .build())) //
+ .withCopyFileToContainer(MountableFile.forClasspathResource(TEST_RESOURCES + "/bob_key.pub"),
+ "/home/bob/.ssh/authorized_keys")
+ // entrypoint must be executable. Spotbugs doesn't like 0777, so use hex
+ .withCopyFileToContainer(
+ MountableFile.forClasspathResource(TEST_RESOURCES + "/entrypoint.sh", 0x1ff),
+ "/entrypoint.sh")
+ .waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*\\n", 1)) //
+ .withExposedPorts(22) //
+ .withLogConsumer(new Slf4jLogConsumer(LOG));
+ sshdContainer.start();
+ try {
+ FileKeyPairProvider keyPairProvider = CommonTestSupportUtils.createTestKeyPairProvider(TEST_RESOURCES + "/bob_key");
+ client.setKeyIdentityProvider(keyPairProvider);
+ client.start();
+ try (ClientSession session = client.connect("bob", sshdContainer.getHost(), sshdContainer.getMappedPort(22))
+ .verify(CONNECT_TIMEOUT).getSession()) {
+ session.auth().verify(AUTH_TIMEOUT);
+ assertTrue("Should authenticate", session.isAuthenticated());
+ assertTrue("Unexpected session type " + session.getClass().getName(), session instanceof TestSession);
+ assertEquals("Unexpected strict KEX usage", withStrictKex, ((TestSession) session).usesStrictKex());
+ try (ChannelShell channel = session.createShellChannel()) {
+ channel.setOut(System.out);
+ channel.setErr(System.err);
+ channel.setStreaming(StreamingChannel.Streaming.Sync);
+ PipedOutputStream pos = new PipedOutputStream();
+ PipedInputStream pis = new PipedInputStream(pos);
+ channel.setIn(pis);
+ assertTrue("Could not open session", channel.open().await(DEFAULT_TIMEOUT));
+ LOG.info("writing some data...");
+ pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
+ assertTrue("Channel should be open", channel.isOpen());
+ assertTrue(session.reExchangeKeys().verify(CONNECT_TIMEOUT).isDone());
+ assertTrue("Channel should be open", channel.isOpen());
+ LOG.info("writing some data...");
+ pos.write("\n\n".getBytes(StandardCharsets.UTF_8));
+ assertTrue("Channel should be open", channel.isOpen());
+ channel.close(true);
+ }
+ }
+ } finally {
+ sshdContainer.stop();
+ }
+ }
+
+ // Subclass ClientSessionImpl to get access to the strictKex flag.
+
+ private static class TestSessionFactory extends SessionFactory {
+
+ TestSessionFactory(ClientFactoryManager client) {
+ super(client);
+ }
+
+ @Override
+ protected ClientSessionImpl doCreateSession(IoSession ioSession) throws Exception {
+ return new TestSession(getClient(), ioSession);
+ }
+ }
+
+ private static class TestSession extends ClientSessionImpl {
+
+ TestSession(ClientFactoryManager client, IoSession ioSession) throws Exception {
+ super(client, ioSession);
+ }
+
+ boolean usesStrictKex() {
+ return strictKex;
+ }
+ }
+}
diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/bob_key b/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/bob_key
new file mode 100644
index 000000000..b5b70aeaa
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/bob_key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAxY3Hr1SqpJIQ9SbFfGMGweVy8jg2TEH3GC1K0LudQHJwogRi
++debdCqUtuSITbpPhjkeZSk9rq198d6RhT6TQmY9J8wLL2/+VXZk/rMVEEjeXQS3
+ImRnL2vVmkAunv6LwfDGHIovkhwj3/lqGWphDAKnHyXusPDwQ3N4LFGgxwXvRGqc
+lzmP8H+KDWaaPapk1AZCBIoD4JbL8faBtLNU01r+pB3sIKvfsPJ5DxPErThfrPuD
+qIbA3axEqFlgX4aVl3yMnSWjfhLhO7xD3YwrtUhannHt8pZQo5FkwCGWDpkG3xs+
+qK3ZACrhMFMTvPuDS83jDtEzNd5KYb4KnkOPMQIDAQABAoIBAQCE5GktgrD/39pU
+b25tzFehW25FjpbIGZ/UvbMUUwDnd5RZCMZj9yv1qyc7GOSwFOKmEgpmVqXNuZt9
+dxFBJuT8x7Xf7Zygnp/icbBivakvuTUMMb3X/t6CwfGAwCgcgHMXVZaPYE275f4k
+Dq3Wxv7di3NMusGkeY/GcAipF4gmGKKe7Ck1ifRypF2cDJsgTtsoFUHNNKfnT3gf
+OcJsVLRl0osbsxdqU+Tep46+jHrNt8J9n2VeRNRIqGHj0CkNdpLQOs+MjvIO3Hgq
+9NUxwIExwaPnBpTLlWwfemCz3JQnlAineMbYBGa1tpAA3Iw56NWcNbiOPyUyffbI
+wBC4r1uZAoGBAPESsergFD+ontChEI+h38oM/D9DKCObZR2kz6WArZ54i1dJWOgh
+HCsuxgPjxmaddPKghfNhUORdZBynuS5G7n6BfItNilDiFm2KBk12d38OVovUFo1Q
+r5akclKf0kFxHt5TzHIrNAv7B4OF0Uk3kuDHM7ITX3qDpTSBLlzPAUUHAoGBANHJ
+QIPmuF2q+PXnnSgdEyiETfl/IqUTXQyxda8kRIPJKKHZKPHZePhgJKUq9VP32PrP
+AxIBNrS3Netsp+EAApj09hmWUcgJRIU1/wjpVGqUmguYgh8nVFOPDudOJD5ltQ/A
+enzQ19IkGroaQB8CBGZsPaBAvqRZ5PLbm+BZEPQHAoGAblaMMGCXY/udlQfjOJpy
+f1wqKBpoyMNbKJJCqBGZZaruu+jKVJSy++DQqP8b0+PFnzdxl8+24o8MP0FVNKUq
+i6RgiLHY2ORiN4ixEctjLjg1zJIqMEv50g06di7IYUORSVk5fhfgHourCLu66rQQ
++eiy9JKBZOXUO4/U1I26mwkCgYAhfuCuLsiBLCtUGAcfwISuk3FfxMzjTpQs0qjX
+rhLCd/vk26eN9gs6nR88v/8ryQb8BNGYrljtwdL6I/8qDbZcdcBVlYq5RcGLA3QV
+GCxCWDfAYjlkgAMW1GCsze07iUG/ohvskevjwaAC1u4mBUxujhnI3I2T8EZ+AFKD
+H7V1QQKBgQDNt+zjSdLtA9AczxDwWmi5SbS+k+nGbi6AQO9i73wky/wxx7FonfWS
+2skkOUIst3HBc0Oz+CJTfNFQK6GVqtzTdlZFhMYS0ua1Djd6q6S648+K0cieY4r5
+5irivHYVN8t7lBcvbA7E7yD6dHXSHsn6yOLTrV382qRfJTbxG7ZVWA==
+-----END RSA PRIVATE KEY-----
diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/bob_key.pub b/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/bob_key.pub
new file mode 100644
index 000000000..efecd1b08
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/bob_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFjcevVKqkkhD1JsV8YwbB5XLyODZMQfcYLUrQu51AcnCiBGL515t0KpS25IhNuk+GOR5lKT2urX3x3pGFPpNCZj0nzAsvb/5VdmT+sxUQSN5dBLciZGcva9WaQC6e/ovB8MYcii+SHCPf+WoZamEMAqcfJe6w8PBDc3gsUaDHBe9EapyXOY/wf4oNZpo9qmTUBkIEigPglsvx9oG0s1TTWv6kHewgq9+w8nkPE8StOF+s+4OohsDdrESoWWBfhpWXfIydJaN+EuE7vEPdjCu1SFqece3yllCjkWTAIZYOmQbfGz6ordkAKuEwUxO8+4NLzeMO0TM13kphvgqeQ48x user01
diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/entrypoint.sh b/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/entrypoint.sh
new file mode 100644
index 000000000..26489c5f0
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/common/kex/extensions/client/entrypoint.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+chown -R bob /home/bob
+chmod 0600 /home/bob/.ssh/*
+
+/usr/sbin/sshd -D -ddd
diff --git a/sshd-mina/pom.xml b/sshd-mina/pom.xml
index 967b12930..6d1c4ed6a 100644
--- a/sshd-mina/pom.xml
+++ b/sshd-mina/pom.xml
@@ -124,6 +124,7 @@
**/SessionReKeyHostKeyExchangeTest.java
**/HostBoundPubKeyAuthTest.java
**/PortForwardingWithOpenSshTest.java
+ **/StrictKexInteroperabilityTest.java
**/OpenSSHCertificateTest.java
diff --git a/sshd-netty/pom.xml b/sshd-netty/pom.xml
index 5d774029f..ac34b5094 100644
--- a/sshd-netty/pom.xml
+++ b/sshd-netty/pom.xml
@@ -143,6 +143,7 @@
**/SessionReKeyHostKeyExchangeTest.java
**/HostBoundPubKeyAuthTest.java
**/PortForwardingWithOpenSshTest.java
+ **/StrictKexInteroperabilityTest.java
**/OpenSSHCertificateTest.java