Mutual TLS (mTLS) strengthens security by forcing both client and server to present valid X.509 certificates during
the TLS handshake.
This guide shows how to
- generate server-side and client-side Java KeyStores (JKS)
- make the two parties trust each other
- wire everything into Spring Integration, and
- reject connections whose certificate CN is not the expected value
1. Generate keys and certificates with keytool
1 . 1 Create the server keystore
keytool -genkeypair \
-alias server \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-keystore server.jks \
-storepass passwordLocal \
-dname "CN=server.example.com,OU=Dev,O=Example,L=City,S=State,C=US"
1 . 2 Create the client keystore
keytool -genkeypair \
-alias client \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-keystore client.jks \
-storepass passwordLocal \
-dname "CN=client.example.com,OU=Dev,O=Example,L=City,S=State,C=US"
1 . 3 Export and exchange the certificates
-
Export the server certificate and import it into the client trust store:
keytool -exportcert -alias server -keystore server.jks \ -storepass passwordLocal -file server.crt keytool -importcert -alias server -keystore client.jks \ -storepass passwordLocal -file server.crt -noprompt
-
Export the client certificate and import it into the server trust store:
keytool -exportcert -alias client -keystore client.jks \ -storepass passwordLocal -file client.crt keytool -importcert -alias client -keystore server.jks \ -storepass passwordLocal -file client.crt -noprompt
After these steps each side trusts the certificate presented by its peer.
2. Wire mTLS into Spring Integration
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-ip="http://www.springframework.org/schema/integration/ip"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/ip
http://www.springframework.org/schema/integration/ip/spring-integration-ip.xsd">
<context:annotation-config/>
<!-- 1. SSL context with strict CN checking -->
<bean id="sslContextSupport"
class="mtls.CustomTcpSSLContextSupport">
<!-- keyStore, trustStore, keyStorePwd, trustStorePwd, expectedCN -->
<constructor-arg value="classpath:server.jks"/>
<constructor-arg value="classpath:server.jks"/>
<constructor-arg value="passwordLocal"/>
<constructor-arg value="passwordLocal"/>
<!-- server expects the client’s CN -->
<constructor-arg value="client.example.com"/>
</bean>
<!-- 2. Force the server to demand a client certificate -->
<bean id="socketSupport" class="mtls.CustomSocketSupport"/>
<!-- 3. TCP server that echoes what it gets -->
<int-ip:tcp-connection-factory id="serverCF"
type="server"
port="9876"
serializer="crlfSerializer"
deserializer="crlfSerializer"
ssl-context-support="sslContextSupport"
socket-support="socketSupport"
single-use="false"/>
<bean id="crlfSerializer"
class="org.springframework.integration.ip.tcp.serializer.ByteArrayLfSerializer">
<property name="maxMessageSize" value="8192"/>
</bean>
<int:channel id="inChannel"/>
<int:channel id="outChannel"/>
<int-ip:tcp-inbound-channel-adapter id="inAdapter"
connection-factory="serverCF"
channel="inChannel"/>
<int-ip:tcp-outbound-channel-adapter id="outAdapter"
connection-factory="serverCF"
channel="outChannel"/>
<int:service-activator input-channel="inChannel"
output-channel="outChannel"
ref="echoService"
method="handleMessage"/>
<bean id="echoService" class="mtls.EchoService"/>
</beans>
3. SocketSupport: require a client certificate
package mtls;
import org.springframework.integration.ip.tcp.connection.DefaultTcpSocketSupport;
import javax.net.ssl.SSLServerSocket;
import java.net.ServerSocket;
public class CustomSocketSupport extends DefaultTcpSocketSupport {
@Override
public void postProcessServerSocket(ServerSocket serverSocket) {
if (serverSocket instanceof SSLServerSocket ssl) {
ssl.setNeedClientAuth(true);
}
}
}
4. SSL context that enforces strict CN validation
CustomTcpSSLContextSupport
now wraps every X509TrustManager
with StrictCNTrustManager
, rejecting certificates
whose CN is not the expected value.
package mtls;
import org.springframework.integration.ip.tcp.connection.TcpSSLContextSupport;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
import javax.net.ssl.*;
import java.io.IOException;
import java.security.*;
import java.util.Arrays;
public class CustomTcpSSLContextSupport implements TcpSSLContextSupport {
private static final String STORE_TYPE = "JKS";
private final Resource keyStore;
private final Resource trustStore;
private final char[] keyStorePassword;
private final char[] trustStorePassword;
private final String expectedCN;
private String protocol = "TLS";
public CustomTcpSSLContextSupport(
String keyStore,
String trustStore,
String keyStorePassword,
String trustStorePassword,
String expectedCN) {
Assert.hasText(expectedCN, "'expectedCN' must not be empty");
PathMatchingResourcePatternResolver resolver =
new PathMatchingResourcePatternResolver();
this.keyStore = resolver.getResource(keyStore);
this.trustStore = resolver.getResource(trustStore);
this.keyStorePassword = keyStorePassword.toCharArray();
this.trustStorePassword = trustStorePassword.toCharArray();
this.expectedCN = expectedCN;
}
@Override
public SSLContext getSSLContext() throws GeneralSecurityException, IOException {
KeyStore ks = KeyStore.getInstance(STORE_TYPE);
KeyStore ts = KeyStore.getInstance(STORE_TYPE);
ks.load(keyStore.getInputStream(), keyStorePassword);
ts.load(trustStore.getInputStream(), trustStorePassword);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, keyStorePassword);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
TrustManager[] strict =
Arrays.stream(tmf.getTrustManagers())
.map(tm -> tm instanceof X509TrustManager
? new StrictCNTrustManager((X509TrustManager) tm, expectedCN)
: tm)
.toArray(TrustManager[]::new);
SSLContext ctx = SSLContext.getInstance(protocol);
ctx.init(kmf.getKeyManagers(), strict, null);
return ctx;
}
}
StrictCNTrustManager
This version wraps an existing trust manager and then enforces the CN.
package mtls;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
final class StrictCNTrustManager implements X509TrustManager {
private final X509TrustManager delegate;
private final String expectedCN;
StrictCNTrustManager(X509TrustManager delegate, String expectedCN) {
this.delegate = delegate;
this.expectedCN = expectedCN;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
delegate.checkClientTrusted(chain, authType);
enforceCN(chain[0]);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
delegate.checkServerTrusted(chain, authType);
enforceCN(chain[0]);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return delegate.getAcceptedIssuers();
}
private void enforceCN(X509Certificate cert) throws CertificateException {
String dn = cert.getSubjectX500Principal().getName();
String actual = null;
try {
LdapName ldap = new LdapName(dn);
for (Rdn rdn : ldap.getRdns()) {
if ("CN".equalsIgnoreCase(rdn.getType())) {
actual = rdn.getValue().toString();
break;
}
}
} catch (Exception e) {
throw new CertificateException("Could not parse certificate DN", e);
}
if (!expectedCN.equals(actual)) {
throw new CertificateException(
"Certificate CN mismatch. Expected '" + expectedCN +
"', got '" + actual + "'");
}
}
}
5. Quick test with openssl
-
Export the server certificate (for trust) and the client certificate + key (for authentication) to PEM:
# trusted CA file for the client keytool -exportcert -alias server -keystore server.jks \ -storepass passwordLocal -rfc -file server.pem # client cert & key (in one PEM for quick testing) keytool -exportcert -alias client -keystore client.jks \ -storepass passwordLocal -rfc -file client.pem
-
Start your Spring application, then in another terminal run
openssl s_client \ -connect localhost:9876 \ -cert client.pem \ -key client.pem \ -CAfile server.pem \ -quiet
-
Type a line followed by Enter; the server should echo it back.
If the CN inclient.pem
is notclient.example.com
, the handshake fails with certificate verify failed.