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

  1. 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
    
  2. 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

  1. 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
    
  2. 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
    
  3. Type a line followed by Enter; the server should echo it back.
    If the CN in client.pem is not client.example.com, the handshake fails with certificate verify failed.