Configure Spring Boot’s with custom SSLContext programmatically (for mTLS)

Tags: , , , ,



Problem

Programmatically configure Spring Boot’s to use my custom SSLContext. And use for mTLS.

Context

Spring’s documentation provides only one, clear way how to configure SSL (via application.properties):

server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=secret
server.ssl.trust-store=classpath:truststore.jks
...

However, this solution lacks depth as there are scenarios, where I’d like to advantage of customizing my own SSLContext. For example, configuring mTLS to trust not only single certificate generated via keytool, but to trust both my self-signed certificate and ones placed within Java’s default TrustStore (lib/security/cacerts).

Of course, I could combine them using already mentioned keytool, but I’m looking for more flexible approach, hence providing my own SSLContext.

Spring provides a section on Configure the Web Server, which says to use either something like TomcatServletWebServerFactory or ConfigurableServletWebServerFactory, but they do not really go into depth.

I’ve tried creating a Component:

@Component
public class CustomServletCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    private static final String SERVER_CERTIFICATE_PATH = "identity.jks";
    private static final char[] PASSWORD = "secret".toCharArray();

    private static final String CLIENT_CERTIFICATE_PATH = "certs/client.cer";

    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        factory.setSslStoreProvider(new SslStoreProvider() {
            @Override
            public KeyStore getKeyStore() throws Exception {
                var certificateAsInputStream = this.getClass().getClassLoader().getResourceAsStream(SERVER_CERTIFICATE_PATH);
                var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                keyStore.load(certificateAsInputStream, PASSWORD);

                return keyStore;
            }

            @Override
            public KeyStore getTrustStore() throws Exception {
                var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                keyStore.load(null);
                var organisationRootCertBytes = this.getClass().getClassLoader().getResourceAsStream(CLIENT_CERTIFICATE_PATH).readAllBytes();
                var certificateFactory = CertificateFactory.getInstance("X.509");
                var certificate = certificateFactory.generateCertificate(new ByteArrayInputStream(organisationRootCertBytes));
                keyStore.setCertificateEntry("server", certificate);

                return keyStore;
            }
        });
    }
}

But to no avail.

Answer

This is unfortunately not possible with Spring Boot + Tomcat. It does not have an option to inject a SSLContext, or other properties such as SSLServerSocketFactory or TrustManager or KeyManager.

However if you still want to use Spring Boot and want to configure it fully and if you don’t care what kind of server is used under the covers by Spring boot I would recommend Jetty.

Below is an basic implementation how you can accomplish this with Spring Boot + Jetty:

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;

import java.util.Collections;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public SslContextFactory.Server sslContextFactory() {
        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
        sslContextFactory.setSslContext(sslContext);
        sslContextFactory.setIncludeProtocols(protocols);
        sslContextFactory.setIncludeCipherSuites(ciphers);
        sslContextFactory.setNeedClientAuth(true);
        return sslContextFactory;
    }

    @Bean
    public ConfigurableServletWebServerFactory webServerFactory(SslContextFactory.Server sslContextFactory) {
        JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
        factory.setPort(8443);

        JettyServerCustomizer jettyServerCustomizer = server -> server.setConnectors(new Connector[]{new ServerConnector(server, sslContextFactory)});
        factory.setServerCustomizers(Collections.singletonList(jettyServerCustomizer));
        return factory;
    }

}

You also need to tell spring to not use tomcat anymore and switch to jetty. You can do that by adding the following snippet to your pom:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>


Source: stackoverflow