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.
Advertisement
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>