Home | Send Feedback | Share on Bluesky |

mTLS with Spring Boot and Spring Security

Published: 13. August 2025  •  java, spring

In this blog post, we will explore how to establish a Mutual TLS (mTLS) connection with Spring Boot and then add an authentication layer with Spring Security that validates the client's X.509 certificate.

X.509

X.509 is a standard that defines the format of public key certificates. These certificates are used in various network protocols, including TLS, to provide a means of establishing a secure connection between clients and servers. An X.509 certificate contains the public key of the entity it represents, along with information about the entity and the digital signature of a trusted certificate authority (CA).

The most common use of X.509 certificates is in HTTP over TLS (HTTPS). In this context, the client (e.g., a web browser) verifies the server's identity by checking its X.509 certificate against a list of trusted certificate authorities.

Mutual TLS (mTLS)

With TLS, only the server presents its certificate to the client, so only the client verifies the server's identity. With mTLS, the server also requests a certificate from the client, allowing both parties to authenticate each other.

Benefits:


Downsides

While mTLS provides strong security benefits, it also comes with several challenges and downsides:

Despite these challenges, many organizations find that the security benefits of mTLS outweigh the operational complexity, especially in zero-trust architectures and high-security environments.

mTLS Handshake Process

mTLS builds on top of the TLS handshake process. The following is a simplified overview of the mTLS handshake, with green boxes highlighting the additional steps that mTLS introduces.

ServerClientServerClientClient sends supported TLS version, cipher suites, and client random.Server selects TLS version, cipher suite, and sends server random.Server sends its X.509 certificate containing public key.mTLS: Server requests client certificate for mutual authentication.Server signals end of server hello phase.6. Verify Server CertificateClient verifies server certificate against trusted CAs locally.mTLS: Client sends its X.509 certificate.Client generates premaster secret, encrypts with server's public key.mTLS: Client proves ownership of private key by signing handshake data.10a. Generate Master SecretFrom premaster secret + client/server randoms10b. Generate Master SecretFrom premaster secret + client/server randomsClient ready to switch to symmetric encryption.Client sends encrypted hash of all handshake messages.13. Verify Client CertificatemTLS: Server verifies client certificate against trusted CAs locally.Server ready to switch to symmetric encryption.Server sends encrypted hash of all handshake messages.1. Client Hello2. Server Hello3. Certificate (Server's X.509)4. Certificate Request5. Server Hello Done7. Certificate (Client's X.509)8. Client Key Exchange9. Certificate Verify11. Change Cipher Spec12. Finished14. Change Cipher Spec15. Finished
  1. Client Hello
    The client starts the handshake by sending a "Client Hello" message. This includes the TLS version it supports, a list of cipher suites, and a random value. This begins the negotiation process.

  2. Server Hello
    The server responds with a "Server Hello" message. It selects the highest TLS version and strongest cipher suite supported by both, and sends its own random value.

  3. Certificate (Server's X.509)
    The server sends its X.509 certificate to the client. This certificate contains the server's public key and identity, signed by a trusted Certificate Authority (CA). The client uses this to authenticate the server.

  4. Certificate Request
    The server sends a "Certificate Request" message, asking the client to provide its X.509 certificate. This is a key part of mutual TLS, enabling the server to authenticate the client.

  5. Server Hello Done
    The server sends a "Server Hello Done" message, indicating it has finished its initial handshake messages.

  6. Verify Server Certificate
    The client verifies the server's X.509 certificate. It checks the certificate's validity (dates, domain name) and confirms the digital signature using the CA's public key (from its truststore). If valid, the server is authenticated.

  7. Certificate (Client's X.509)
    The client sends its own X.509 certificate to the server. This certificate contains the client's public key and identity, also signed by a CA. The server will use this to authenticate the client.

  8. Client Key Exchange
    The client generates a "premaster secret," encrypts it with the server's public key (from the server's certificate), and sends it to the server. Only the server, with its private key, can decrypt this.

  9. Certificate Verify
    The client sends a "Certificate Verify" message. This is a digital signature of all previous handshake messages, created using the client's private key. This proves the client possesses the private key corresponding to its certificate, thus authenticating the client.

  10. Generate Master Secret
    Both client and server generate the master secret locally from the premaster secret and client/server randoms.

  11. Change Cipher Spec (Client)
    The client sends a "Change Cipher Spec" notification, indicating all future messages from the client will be encrypted.

  12. Finished (Client)
    The client sends a "Finished" message encrypted with the newly negotiated symmetric keys.

  13. Verify Client Certificate
    The server verifies the client's X.509 certificate. It checks the certificate's validity and signature using the CA's public key. It also verifies the "Certificate Verify" signature using the client's public key (from the client's certificate). If valid, the client is authenticated.

  14. Change Cipher Spec (Server)
    The server sends "Change Cipher Spec," indicating all future messages from the server will be encrypted.

  15. Finished (Server)
    The server sends a "Finished" message encrypted with the symmetric keys. The handshake is now complete, and all further communication is encrypted and authenticated with the shared symmetric keys.

Setting up key stores

The following Spring Boot example requires a set of certificates and keys for mutual TLS (mTLS) authentication.

I use task to run all the necessary steps. For setting up the infrastructure, the task requires the openSSL command-line tool and the keytool from the Java Development Kit (JDK).

The following task sets up the certificate authority (CA) and generates the necessary certificates and keys. Note that while this example uses the same CA for both client and server certificates, you can use different CAs.

  generate-ca:
    cmds:
      - echo "Creating Certificate Authority (CA) with {{.CRYPTO_ALGORITHM}}..."
      - openssl genpkey -algorithm {{.CRYPTO_ALGORITHM}} -out ca-key.pem
      - cmd: openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days {{.VALIDITY_DAYS}} -subj "/C=CH/ST=BE/L=Bern/O=Demo AG/OU=IT Department/CN=Demo CA"
        platforms: [windows]
      - cmd: openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days {{.VALIDITY_DAYS}} -subj '/C=CH/ST=BE/L=Bern/O=Demo AG/OU=IT Department/CN=Demo CA'
        platforms: [linux, darwin]

Taskfile.yml

The following two tasks create the public and private keys for the server and client, respectively, and then generate and sign their X.509 certificates with the CA. This demo uses the Ed448 elliptic curve cryptography algorithm, but other algorithms like RSA or Ed25519 are also supported by Java and OpenSSL.

  generate-server-cert:
    cmds:
      - echo "Creating server certificate with {{.CRYPTO_ALGORITHM}}..."
      - openssl genpkey -algorithm {{.CRYPTO_ALGORITHM}} -out server-key.pem
      - cmd: openssl req -new -key server-key.pem -out server.csr -subj "/C=CH/ST=BE/L=Bern/O=Demo AG/OU=Server/CN=localhost"
        platforms: [windows]
      - cmd: openssl req -new -key server-key.pem -out server.csr -subj '/C=CH/ST=BE/L=Bern/O=Demo AG/OU=Server/CN=localhost'
        platforms: [linux, darwin]
      - openssl x509 -req -in server.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days {{.VALIDITY_DAYS}}

Taskfile.yml

  generate-client-cert:
    cmds:
      - echo "Creating client certificate with {{.CRYPTO_ALGORITHM}}..."
      - openssl genpkey -algorithm {{.CRYPTO_ALGORITHM}} -out client-key.pem
      - cmd: openssl req -new -key client-key.pem -out client.csr -subj "/C=CH/ST=BE/L=Bern/O=Demo AG/OU=Client/CN=demo-client"
        platforms: [windows]
      - cmd: openssl req -new -key client-key.pem -out client.csr -subj '/C=CH/ST=BE/L=Bern/O=Demo AG/OU=Client/CN=demo-client'
        platforms: [linux, darwin]
      - openssl x509 -req -in client.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days {{.VALIDITY_DAYS}}

Taskfile.yml

Lastly, all these keys and certificates need to be imported into keystores.

  create-keystores:
    cmds:
      - echo "Creating keystores..."
      - openssl pkcs12 -export -in server-cert.pem -inkey server-key.pem -out keystore.p12 -name {{.SERVER_ALIAS}} -CAfile ca-cert.pem -caname root -passout pass:{{.KEYSTORE_PASSWORD}}
      - openssl pkcs12 -export -in client-cert.pem -inkey client-key.pem -out client-keystore.p12 -name {{.CLIENT_ALIAS}} -CAfile ca-cert.pem -caname root -passout pass:{{.KEYSTORE_PASSWORD}}
      - echo "Creating server truststore (contains CA and client certificates)..."
      - keytool -import -file ca-cert.pem -alias {{.CA_ALIAS}} -keystore server-truststore.p12 -storetype PKCS12 -storepass {{.KEYSTORE_PASSWORD}} -noprompt
      - keytool -import -file client-cert.pem -alias {{.CLIENT_ALIAS}} -keystore server-truststore.p12 -storetype PKCS12 -storepass {{.KEYSTORE_PASSWORD}} -noprompt
      - echo "Creating client truststore (contains only CA certificate)..."
      - keytool -import -file ca-cert.pem -alias {{.CA_ALIAS}} -keystore client-truststore.p12 -storetype PKCS12 -storepass {{.KEYSTORE_PASSWORD}} -noprompt

Taskfile.yml

Note that client-truststore.p12 is not required if you use a CA that is already in the operating system's trust store. For instance, Let's Encrypt is a widely trusted CA, and its certificates are included in most operating systems. In this example, however, we are using a self-signed CA for demonstration, which is not trusted by default.

Similarly, the CA in server-truststore.p12 is only necessary if it is not already trusted by the operating system. What is essential, however, is that every client's certificate is stored in server-truststore.p12, as the server needs these to authenticate clients. It is also crucial that each client has its own unique certificate. In this example, we have only created a single client key pair and certificate.

mTLS with Spring Boot

mTLS is not handled by Spring Boot directly but relies on the embedded servlet container (e.g., Tomcat, Jetty) for support. You can enable mTLS in a Spring Boot application without writing any Java code by adding the following properties to your application.properties or application.yml file. Spring Boot will then configure the embedded server accordingly on startup.

# Server port
server.port=8443

# SSL Configuration
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=changeit
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=server

# Client Certificate Configuration
server.ssl.client-auth=need
server.ssl.trust-store=classpath:truststore.p12
server.ssl.trust-store-password=changeit
server.ssl.trust-store-type=PKCS12

application.properties

Setting server.ssl.enabled to true will enable TLS for the application. TLS support also needs to know where the keystore is located, which contains the server's private key and certificate chain.

For mTLS to work, the embedded server needs access to the truststore, which contains all the client certificates and the CA with which these client certificates are signed.

The server.ssl.client-auth property configures how the embedded server handles client certificate authentication. There are three possible options:

X.509 Authentication with Spring Security

mTLS in Spring Boot works without Spring Security. mTLS is a core feature of the embedded servlet container and therefore part of Spring Boot core. You can use mTLS as is. When setting server.ssl.client-auth to need, you ensure that all clients must present a valid certificate. This might already be enough for your security requirements.

However, Spring Security, when used in conjunction with mTLS, can provide an additional layer of security by enabling fine-grained access control. This allows you to define more specific security policies based on the client's identity, roles, or other attributes present in the certificate.


For this blog post I wrote a simple Spring Boot application with the following three endpoints:

You can find the source code of the example RestController here.


To enable X.509 authentication in a Spring Boot/Spring Security application, create a security configuration class and configure a principal extractor with the x509 method. In this example, the SubjectX500PrincipalExtractor is used to extract the CN (Common Name) from the client's certificate. The UserDetailsService then checks if the extracted CN is authorized to access the secure endpoints.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(
        authz -> authz.requestMatchers("/api/public/**").permitAll()
            .requestMatchers("/api/secure/**").authenticated().anyRequest().permitAll())
        .x509(x509 -> x509.x509PrincipalExtractor(new SubjectX500PrincipalExtractor())
            .userDetailsService(userDetailsService()))
        .csrf(CsrfConfigurer::disable);

    return http.build();
  }

  @Bean
  UserDetailsService userDetailsService() {
    return cn -> {
      // For this demo, we'll accept only certificate with CN == "demo-client"
      if ("demo-client".equals(cn.toLowerCase())) {
        return new User(cn, "", AuthorityUtils.createAuthorityList("ROLE_USER"));
      }
      throw new UsernameNotFoundException("Certificate not authorized: " + cn);
    };
  }
}

SecurityConfig.java

Based on the server.ssl.client-auth setting, the behavior of the application changes:

server.ssl.client-auth=need

With this setting, the server requires all clients to present a valid certificate. This means that even for accessing the public endpoint /api/public/health, a valid certificate is required. However, compared to the secure endpoints, the configuration above allows any client with a valid certificate to access the public endpoint. The secure endpoints can only be accessed by clients with a valid certificate that contains the CN "demo-client".

It is important to note that all checks in Spring Security occur after the client certificate has been presented to the embedded server. Therefore, when the request reaches the Spring Security layer, a valid certificate is always present, and the user has already been authenticated by the embedded server.

In this demo application, we only have one user and one client certificate. Consequently, the check in userDetailsService() is superfluous because only one client can connect to our server, and the CN of this client is always "demo-client".


server.ssl.client-auth=want

With this setting, any client can connect to our server, regardless of whether they present a certificate. This means all clients can access the public endpoint. However, only clients with a valid certificate containing the CN "demo-client" can access the secure endpoints.

The check in userDetailsService() becomes crucial in this scenario because we have clients with and without a certificate, and we need to ensure that only clients with a valid certificate are granted access to the secure endpoints.

mTLS configuration with Java 11 HTTP client

In this section, we take a look at how to configure the key and trust store for the Java 11 HTTP client to enable mTLS.

The Java 11 HTTP client supports mTLS by allowing you to set a custom SSLContext that contains the necessary key and trust stores. The following code snippet demonstrates how to create an SSLContext with a client certificate and a trust store containing the CA certificates.

  private SSLContext createSSLContext() throws Exception {
    // Client certificate
    KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
    try (FileInputStream fis = new FileInputStream(this.clientKeystorePath)) {
      clientKeyStore.load(fis, this.clientKeystorePassword.toCharArray());
    }

    KeyManagerFactory kmf = KeyManagerFactory
        .getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(clientKeyStore, this.clientKeystorePassword.toCharArray());

    // Trust store with CA certificate
    KeyStore trustStore = KeyStore.getInstance("PKCS12");
    try (FileInputStream fis = new FileInputStream(this.truststorePath)) {
      trustStore.load(fis, this.truststorePassword.toCharArray());
    }

    TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);

    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

    return sslContext;
  }

App.java

I already mentioned before that the client-truststore.p12 is optional when the root certificate of the CA is already present in the operating system's trust store. In that case, you can skip the trust store configuration. However, in this example, I created a self-signed root certificate and therefore have to provide the trust store with the CA certificate.

The key store, on the other hand, is essential because it contains the client's private key and the corresponding certificate chain. When the server requests the client's certificate during the TLS handshake, the key store provides the necessary credentials.

After creating the SSLContext, you can use it to create an HttpClient instance that supports mTLS. Here's an example of how to do this:

    SSLContext sslContext = createSSLContext();
    try (HttpClient httpClient = HttpClient.newBuilder().sslContext(sslContext)
        .connectTimeout(Duration.ofSeconds(this.connectionTimeout)).build()) {

App.java

The HttpClient instance can be used to send requests. The client handles the whole mTLS handshake automatically under the hood.

      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(this.serverBaseUrl + "/public/health")).GET().build();

      HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

App.java

Conclusion

Implementing mTLS with Spring Boot provides a robust foundation for zero-trust architectures and high-security applications. While the setup requires careful certificate management and additional operational complexity, the security benefits—particularly the mutual authentication and protection against various attack vectors—make it worthwhile for scenarios requiring strong client authentication.

We have seen that configuring mTLS with Spring Boot is straightforward and can be done without writing any Java code. Spring Security adds a layer on top of mTLS by providing authentication and authorization features.

Also, mTLS with the Java 11 HttpClient is not that complicated. It requires some configuration of the SSL context, but once that's done, the client can handle the mTLS handshake automatically.

The setup I have shown in this post is only feasible for small deployments with a limited number of clients. For handling thousands of clients, a proper Public Key Infrastructure (PKI) is necessary to manage certificates and keys efficiently. You can use tools like HashiCorp Vault, AWS Certificate Manager, or other PKI solutions to automate the certificate issuance and management process.