Skip to content
Ashish.
All posts
Diagram showing mutual TLS handshake between client and server with certificate exchange.
8 min readDevelopmentJava Developers#java#mtls#spring-boot#webclient#restclient#security#ssl#client-certificates

Calling mTLS Services With RestClient and WebClient

Learn how to configure Java's RestClient and WebClient for mutual TLS authentication, handling client certificates and resolving handshake failures in Spring Boot applications.

By Ashish KumarPart 5 of mTLS With Spring Boot

Mutual TLS (mTLS) is often misunderstood as a specific "mTLS mode" in HTTP clients. It is not. mTLS is simply the standard TLS handshake where the client presents a certificate to prove its identity, in addition to the server presenting its certificate to prove its identity. As a core component of application security, mTLS ensures that both parties in a communication channel are verified.

In java, this mechanism relies on two components:

  1. KeyManager: Holds the client’s private key and certificate chain. It answers the server’s request: "Who are you?"
  2. TrustManager: Holds the list of trusted Certificate Authorities (CAs). It answers the client’s question: "Can I trust this server?"

Most developers configure the TrustManager (to trust the server) but forget the KeyManager (to identify themselves). This results in handshake failure. Below, we configure both RestClient and WebClient correctly within a Spring Boot context.

The Mechanism: Building the SslContext

Before configuring any HTTP client, you must build an SslContext. This object encapsulates both the client credentials and the trust store.

Assume you have:

  • client-cert.p12: A PKCS12 keystore containing the client’s private key and certificate.
  • truststore.p12: A keystore containing the CA certificates that sign the server’s certificate.
  • Passwords for both.
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.client.RestClient;
 
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.security.KeyStore;
 
public class MtlsConfig {
 
    public static SslContext createSslContext(
            String clientCertPath, String clientPassword,
            String trustStorePath, String trustPassword) throws Exception {
 
        // 1. Load Client Keystore (for KeyManager)
        KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(clientCertPath)) {
            clientKeyStore.load(fis, clientPassword.toCharArray());
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(clientKeyStore, clientPassword.toCharArray());
 
        // 2. Load Trust Store (for TrustManager)
        KeyStore trustStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(trustStorePath)) {
            trustStore.load(fis, trustPassword.toCharArray());
        }
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);
 
        // 3. Build SslContext
        return SslContextBuilder.forClient()
                .keyManager(kmf) // Provides client identity
                .trustManager(tmf) // Validates server identity
                .build();
    }
}

This SslContext is the artifact you inject into WebClient's Netty-based transport. (For RestClient's JDK-based transport, build an equivalent javax.net.ssl.SSLContext using the same KeyManagerFactory and TrustManagerFactory pattern.)

Configuring RestClient

RestClient (introduced in Spring Framework 6.1) is a synchronous, blocking HTTP client. It uses ClientHttpRequestFactory under the hood. In Spring Boot 3.2+, you can configure it via a RestClient.Builder.

The critical step is passing the SSL configuration to the underlying HTTP client factory. JdkClientHttpRequestFactory wraps a java.net.http.HttpClient, so you configure TLS by building that HttpClient with a standard javax.net.ssl.SSLContext (built the same way as above, via KeyManagerFactory and TrustManagerFactory) and passing it to HttpClient.Builder.sslContext(...). The most robust way in modern Spring Boot is to let Spring manage the bean lifecycle by exposing that configured HttpClient as the request factory.

@Bean
public RestClient mTLSRestClient(SSLContext sslContext) {
    HttpClient httpClient = HttpClient.newBuilder()
            .sslContext(sslContext)
            .build();
 
    return RestClient.builder()
            .requestFactory(new JdkClientHttpRequestFactory(httpClient))
            .baseUrl("https://secure-service.internal")
            .build();
}

Why this works: We wrap a java.net.http.HttpClient configured with our SSLContext inside a JdkClientHttpRequestFactory. When RestClient makes a request, the underlying HttpClient uses this SSLContext to negotiate TLS. The KeyManager inside the context will automatically present the client certificate when the server requests it.

Note: If you are using Spring Boot 3.1 or earlier, RestClient is not available. You should use RestTemplate with a similar ClientHttpRequestFactory configuration. RestTemplate remains a valid choice for legacy systems, but RestClient is the recommended future-proof approach in modern spring-boot applications.

Configuring WebClient

WebClient is reactive and non-blocking. It does not use RestClient’s blocking factory. Instead, it uses a ClientHttpConnector. The default connector in Spring Boot is often Netty-based, which requires a different approach.

You must create a ReactorClientHttpConnector and inject the SslContext into it.

@Bean
public WebClient mTLSWebClient(SslContext sslContext) {
    HttpClient httpClient = HttpClient.create()
            .secure(spec -> spec.sslContext(sslContext));
 
    ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
 
    return WebClient.builder()
            .clientConnector(connector)
            .baseUrl("https://secure-service.internal")
            .build();
}

Mechanism difference: HttpClient from Project Reactor’s Netty module consumes the SslContext via the SslContextSpec callback passed to its .secure() method. When WebClient sends a request, Netty initiates the TLS handshake, using the KeyManager from the context to present the client certificate.

Troubleshooting Handshake Failures

When mTLS fails, the error messages are often generic. You must interpret them based on the TLS handshake flow. Issues often arise when client certificates are misconfigured or missing from the client's trust store.

1. PKIX path building failed or unable to find valid certification path

Mechanism: This error occurs during the server verification phase. The client’s TrustManager cannot build a chain from the server’s certificate to a trusted root CA.

Cause:

  • The server is using a self-signed certificate or a certificate from a CA not in your truststore.p12.
  • The truststore.p12 was not loaded correctly into the SslContext.

Fix:

  • Ensure the server’s CA certificate is imported into the trust store used by the TrustManager.
  • For debugging, temporarily add .trustManager(InsecureTrustManagerFactory.INSTANCE) (not for production) to see if the server cert is the issue.

2. alert handshake_failure or No available certificate or key corresponds to the server cipher suites

Mechanism: This error occurs when the server rejects the client’s certificate. The server requested a client certificate, but the client either:

  • Did not present any certificate (missing KeyManager).
  • Presented a certificate that the server does not trust (client cert not signed by a CA the server trusts).
  • Presented a certificate with incompatible parameters (e.g., RSA key but server only accepts ECDSA).

Cause:

  • The KeyManager was not passed to the SslContext (most common error).
  • The client certificate is expired or revoked.
  • The server requires a specific CA for client authentication, and your client cert is not signed by that CA.

Fix:

  • Verify that keyManager(kmf) is called in SslContextBuilder.forClient().
  • Check the server logs for CertificateVerify failures.
  • Ensure the client certificate’s private key matches the public key in the certificate.

3. Protocol version mismatch or Unsupported cipher suite

Mechanism: The client and server cannot agree on a TLS version or encryption algorithm.

Cause:

  • The server requires TLS 1.3, but the SslContext is configured for TLS 1.2 or lower.
  • The client’s JVM does not support the cipher suites offered by the server.

Fix:

  • Explicitly set the TLS version in the SslContextBuilder:
    .protocols("TLSv1.3")
  • Update your JVM’s security policy if custom cipher suites are disabled.

Conclusion

mTLS in java is not a black box. It is a configuration of two managers: KeyManager for identity and TrustManager for trust. Whether you use RestClient or WebClient, the core task is the same: build an SslContext with both, and inject it into the HTTP client’s transport layer.

  • RestClient: Use a JdkClientHttpRequestFactory wrapping a java.net.http.HttpClient configured with a javax.net.ssl.SSLContext.
  • WebClient: Use ReactorClientHttpConnector with a Netty HttpClient configured with the SslContext.

Always verify both the trust store (server cert) and the key store (client cert) are correctly loaded. Most handshake failures stem from one of these two being misconfigured. For legacy applications still using RestTemplate, ensure the underlying ClientHttpRequestFactory is similarly configured with the appropriate SSL socket factory.

Related posts