Search code examples
javaspring-bootssltomcatsslcontext-kickstart

Configuring SSL programatically of a Spring Boot server with Tomcat is failing


I am trying to configure the ssl configuration of a spring boot with a tomcat embedded server. Spring Boot is by default provided with a tomcat. SSL can be configured with the application properties such as keystore path, truststore path etc. I don't want to configure it with properties, so I just want to configure it programatically. I have developed my own ssl library which I want to use as it has couple of additional feature which I need use. In the past I attempted to configure it but failed, so I switch to either using spring boot with jetty or netty to configure my server ssl programatically. However it was still in my mind to give it another try. Today I am still failing to configure it programatically, maybe someone can help me out.

My stacktrace is:

Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat server
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:229)
    at org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:43)
    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178)
    ... 14 more
Caused by: java.lang.IllegalArgumentException: standardService.connector.startFailed
    at org.apache.catalina.core.StandardService.addConnector(StandardService.java:238)
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:282)
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:213)
    ... 16 more
Caused by: org.apache.catalina.LifecycleException: Protocol handler start failed
    at org.apache.catalina.connector.Connector.startInternal(Connector.java:1077)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.StandardService.addConnector(StandardService.java:234)
    ... 18 more
Caused by: java.lang.IllegalArgumentException: SSLHostConfig attribute certificateFile must be defined when using an SSL connector
    at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:107)
    at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:71)
    at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:235)
    at org.apache.tomcat.util.net.AbstractEndpoint.bindWithCleanup(AbstractEndpoint.java:1227)
    at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1313)
    at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:617)
    at org.apache.catalina.connector.Connector.startInternal(Connector.java:1074)
    ... 20 more
Caused by: java.io.IOException: SSLHostConfig attribute certificateFile must be defined when using an SSL connector
    at org.apache.tomcat.util.net.SSLUtilBase.getKeyManagers(SSLUtilBase.java:312)
    at org.apache.tomcat.util.net.SSLUtilBase.createSSLContext(SSLUtilBase.java:247)
    at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:105)
    ... 26 more

My code looks like this:

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ServerConfig {

    @Bean
    public ServletWebServerFactory servletContainer(SSLConnectorCustomizer sslConnectorCustomizer) {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addConnectorCustomizers(sslConnectorCustomizer);
        return tomcat;
    }

}
import nl.altindag.ssl.SSLFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SSLConfig {

    @Bean
    public SSLFactory sslFactory(@Value("${ssl.keystore-path}") String keyStorePath,
                                 @Value("${ssl.keystore-password}") char[] keyStorePassword,
                                 @Value("${ssl.truststore-path}") String trustStorePath,
                                 @Value("${ssl.truststore-password}") char[] trustStorePassword,
                                 @Value("${ssl.client-auth}") boolean isClientAuthenticationRequired) {

        return SSLFactory.builder()
                .withSwappableIdentityMaterial()
                .withSwappableTrustMaterial()
                .withIdentityMaterial(keyStorePath, keyStorePassword)
                .withTrustMaterial(trustStorePath, trustStorePassword)
                .withDefaultTrustMaterial()
                .withSystemTrustMaterial()
                .withNeedClientAuthentication(isClientAuthenticationRequired)
                .build();
    }

}
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.KeyStoreUtils;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Configuration;

import javax.net.ssl.SSLParameters;
import javax.net.ssl.X509TrustManager;
import java.security.KeyStore;

@Configuration
public class SSLConnectorCustomizer implements TomcatConnectorCustomizer {

    private final SSLFactory sslFactory;

    public SSLConnectorCustomizer(SSLFactory sslFactory) {
        this.sslFactory = sslFactory;
    }

    @Override
    public void customize(Connector connector) {
        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(8444);

        AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
        configureSsl(protocol);
    }

    private void configureSsl(AbstractHttp11Protocol<?> protocol) {
        protocol.setSSLEnabled(true);

        SSLHostConfig sslHostConfig = new SSLHostConfig();
        sslHostConfig.setSslProtocol("TLS");
        sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName());

        configureSslClientAuth(sslHostConfig);

        KeyStore trustStore = sslFactory.getTrustManager()
                .map(X509TrustManager::getAcceptedIssuers)
                .map(KeyStoreUtils::createTrustStore)
                .orElseThrow();
        sslHostConfig.setTrustStore(trustStore);

        SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
        certificate.setSslContext(new SSLContextWrapper(sslFactory));
        sslFactory.getKeyManager().ifPresent(certificate::setCertificateKeyManager);
        sslHostConfig.addCertificate(certificate);

        String ciphers = String.join(",", sslFactory.getCiphers());
        sslHostConfig.setCiphers(ciphers);

        String protocols = String.join(",", sslFactory.getProtocols());
        sslHostConfig.setProtocols(protocols);

        protocol.addSslHostConfig(sslHostConfig);
    }

    private void configureSslClientAuth(SSLHostConfig config) {
        String clientAuth;
        SSLParameters sslParameters = sslFactory.getSslParameters();
        if (sslParameters.getNeedClientAuth()) {
            clientAuth = "required";
        } else if (sslParameters.getWantClientAuth()) {
            clientAuth = "optional";
        } else {
            clientAuth = "none";
        }

        config.setCertificateVerification(clientAuth);
    }

}
import nl.altindag.ssl.SSLFactory;
import org.apache.tomcat.util.net.SSLContext;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.TrustManager;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public final class SSLContextWrapper implements SSLContext {

    private final SSLFactory sslFactory;

    public SSLContextWrapper(SSLFactory sslFactory) {
        this.sslFactory = sslFactory;
    }


    @Override
    public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) {
        // not needed to initialize as it is already initialized
    }

    @Override
    public void destroy() {

    }

    @Override
    public SSLSessionContext getServerSessionContext() {
        return sslFactory.getSslContext().getServerSessionContext();
    }

    @Override
    public SSLEngine createSSLEngine() {
        return sslFactory.getSSLEngine();
    }

    @Override
    public SSLServerSocketFactory getServerSocketFactory() {
        return sslFactory.getSslServerSocketFactory();
    }

    @Override
    public SSLParameters getSupportedSSLParameters() {
        return sslFactory.getSslParameters();
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return sslFactory.getKeyManager()
                .map(keyManager -> keyManager.getCertificateChain("server"))
                .orElseThrow();
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]);
    }

}

I am not able to share the keystores as a file here, so I converted them to a base64 encoded string. So if you want to give it a try on your side you can decode and create the binary file out of it.

identity.jks

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
O3MCAwGGoA==

truststore.jks

MIIEjgIBAzCCBEcGCSqGSIb3DQEHAaCCBDgEggQ0MIIEMDCCBCwGCSqGSIb3DQEH
BqCCBB0wggQZAgEAMIIEEgYJKoZIhvcNAQcBMCkGCiqGSIb3DQEMAQYwGwQUCQPL
V1YP9wunGMWFyv1BwcRTIAACAwDDUICCA9jrocMHqNhm8aHENyLPmEi8qSuSPdut
++OBZvYRI5Bb2fFwye3dsoo9xEcSpDTcjp3cCiPPnGzcaWyvOvyTFtGr7AuUISte
qI2bfHd+bR1e7KhC4a8xZDhpsn7TmYTOHGhHJi7W4Yaw6UbgYhzfE++KpqNMfWBk
t4/4V2n3BNzShtxXDBw9CmT1Fdcv1lJ1j5YWKdgD0/p59ccEZHtiUvY6QOROBfjq
oI6kkMzW/mDJqQDKowLsW2O/UbPaRC1B8Ew0NSlHWhxaU347UFsotW8iKuXVnei0
gPiPfBFSuZ24MFaz6XENgEZcnNCJBeeK/MGma8i1rOADKbEaErzvlwBNm8BWtlC3
Ix4akym2XSy3pqFJrP+1cJJONwsI766IL72PUyXTBRtKEaqSd6jZZGkiyTPsiiK6
G3apckGnKpgJLyR6hrUd2SC8bwf92waRne9VihA0O4CovO9G0ab6ud02bEISVQAt
/n+lnU6FmjK3tGy1P/Kpx1vomkmZkdWqE56dvAQv5DRV7EZQ9TJU8FoAyP4E5RWD
1AQi7uDA233e1Wv4rYzWhwm/0XeFsdKw/N3rjmeNilgG4/KwSFFkl53fNECDDEmS
czcEkA99JU6nJBCuFF8kn0yVXTh+8ezgdSiT8LIUtqbldzKR8uMXFlzCMOCdJc21
cBC7DX9LA+Dz/pkcEj1smtDDSv+zM+DbEA4iJZBlF68xqiZzhrWcmbdtSKN5iUN/
kkLELUUjO76OIdxXILW2GHLHSe01fbqpHeY9NC732WmF329+ucODzCq9BjxARpAm
bCEX8tbcOrCqm3947JuSbgW6CxzDd2gzmYoE6d4LSfWLRgQ4SdLpgieVhfbsunBk
lnvCrnsa/VAQnEMZEzWCgvRAcuRs6bPvUXXFPwrz8OvQoPmdDRA4uWYfhEZZJjq4
uRGQRt+Bf4v5H1e2yzxSfRKlecJlhpVMyZtNsWTaNgZFX+c1SfzXXZGGMgLBEh9t
wLFdDWA848eXP97te7boqusR/UTXd6hLDmU5bb5NmjzY92aEK3LfIeLhFlKjWQKF
FooNxvKA/jyqUt/LxffAxK4co8ivyLzOMdHsPsqTjpkqjro5RgnYJTJWhBLMois5
sJej6EphOUzBMeEwgdlEs3pbT3/yp8QrhYKUG2HjkUVE2GKVwWNcjRfguGGK8MYC
B3ztnwPeAblpaPi4aQY55maYGXR3SPTAX2MVGScBfL8GhDZGr/yf7441wi6YntOH
CYts3VW/0zHAXNTaycdUTSMg9f7O312aYrk59Hd5Yfiw1StzMtdJMnMxlOdgTP33
w+wwPjAhMAkGBSsOAwIaBQAEFDdfecmJ7H2HFI93DLhyZ4r8Mx2iBBT/uNLzzuIO
SZI0e5jSFcFmOoYSvwIDAYag

These two base64 encoded strings can be tranformed into binary files with the snippet bellow. Please make sure to replace the empty content with the above base64 encoded strings.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Base64;

public class Base64DecoderUtils {

    public static void main(String[] args) throws IOException {
        String identityContentAsBase64Encoded = "";
        String trustStoreContentAsBase64Encoded = "";
        
        Path identityPath = Paths.get("/path/to/identity.jks");
        Path trustStorePath = Paths.get("/path/to/truststore.jks");
        
        byte[] decoded = Base64.getDecoder().decode(String.join("", identityContentAsBase64Encoded.split(System.lineSeparator())));
        Files.write(identityPath, decoded, StandardOpenOption.CREATE);

        decoded = Base64.getDecoder().decode(String.join("", trustStoreContentAsBase64Encoded.split(System.lineSeparator())));
        Files.write(trustStorePath, decoded, StandardOpenOption.CREATE);
    }
}

The password of these to files are: secret

my application property file is:

server:
  port: 8443

ssl:
  client-auth: true
  keystore-path: identity.jks
  keystore-password: secret
  truststore-path: truststore.jks
  truststore-password: secret

I am using the following dependencies:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart</artifactId>
    <version>8.1.7</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
    <version>2.7.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.5</version>
</dependency>

Update 2023-10-23

The supplied configuration such as keymanager or sslcontext is being ignored. It seems like it was never the intention of the developers/maintainers to allow such custom configuration. I opened a pull request here to enable it in tomcat: https://github.com/apache/tomcat/pull/673

However I don't know whether the maintainers will allow that kind of option to configure the server with a custom sslcontext.


Solution

  • The maintainers of Tomcat have adjusted the code in the following git commit Allow user provided SSLContext instances on SSLHostConfigCertificate, which enables to programatically configure the ssl configuration of the server. I have a working example as a demo here: GitHub: Spring with Tomcat and custom ssl configuration

    I ended up having configuration below.

    import nl.altindag.ssl.SSLFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class SSLConfig {
    
        @Bean
        public SSLFactory sslFactory(@Value("${ssl.keystore-path}") String keyStorePath,
                                     @Value("${ssl.keystore-password}") char[] keyStorePassword,
                                     @Value("${ssl.truststore-path}") String trustStorePath,
                                     @Value("${ssl.truststore-password}") char[] trustStorePassword,
                                     @Value("${ssl.client-auth}") boolean isClientAuthenticationRequired) {
    
            return SSLFactory.builder()
                    .withSwappableIdentityMaterial()
                    .withSwappableTrustMaterial()
                    .withIdentityMaterial(keyStorePath, keyStorePassword)
                    .withTrustMaterial(trustStorePath, trustStorePassword)
                    .withNeedClientAuthentication(isClientAuthenticationRequired)
                    .build();
        }
    
    }
    
    
    import nl.altindag.ssl.SSLFactory;
    import org.apache.catalina.connector.Connector;
    import org.apache.coyote.http11.AbstractHttp11Protocol;
    import org.apache.tomcat.util.net.SSLHostConfig;
    import org.apache.tomcat.util.net.SSLHostConfigCertificate;
    import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class SSLConnectorCustomizer implements TomcatConnectorCustomizer {
    
        private final SSLFactory sslFactory;
        private final int port;
    
        public SSLConnectorCustomizer(SSLFactory sslFactory, @Value("${server.port}") int port) {
            this.sslFactory = sslFactory;
            this.port = port;
        }
    
        @Override
        public void customize(Connector connector) {
            connector.setScheme("https");
            connector.setSecure(true);
            connector.setPort(port);
    
            AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
            protocol.setSSLEnabled(true);
    
            SSLHostConfig sslHostConfig = new SSLHostConfig();
            SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
            certificate.setSslContext(new TomcatSSLContext(sslFactory));
            sslHostConfig.addCertificate(certificate);
            protocol.addSslHostConfig(sslHostConfig);
        }
    
    }
    
    import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
    import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ServerConfig {
    
        @Bean
        public ServletWebServerFactory servletContainer(SSLConnectorCustomizer sslConnectorCustomizer) {
            TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
            tomcat.addConnectorCustomizers(sslConnectorCustomizer);
            return tomcat;
        }
    
    }
    
    import nl.altindag.ssl.SSLFactory;
    import org.apache.tomcat.util.net.SSLContext;
    
    import javax.net.ssl.KeyManager;
    import javax.net.ssl.SSLEngine;
    import javax.net.ssl.SSLParameters;
    import javax.net.ssl.SSLServerSocketFactory;
    import javax.net.ssl.SSLSessionContext;
    import javax.net.ssl.TrustManager;
    import java.security.SecureRandom;
    import java.security.cert.X509Certificate;
    
    public final class TomcatSSLContext implements SSLContext {
    
        private final SSLFactory sslFactory;
    
        public TomcatSSLContext(SSLFactory sslFactory) {
            this.sslFactory = sslFactory;
        }
    
        @Override
        public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) {
            // not needed to initialize as it is already initialized
        }
    
        @Override
        public void destroy() {
    
        }
    
        @Override
        public SSLSessionContext getServerSessionContext() {
            return sslFactory.getSslContext().getServerSessionContext();
        }
    
        @Override
        public SSLEngine createSSLEngine() {
            return sslFactory.getSSLEngine();
        }
    
        @Override
        public SSLServerSocketFactory getServerSocketFactory() {
            return sslFactory.getSslServerSocketFactory();
        }
    
        @Override
        public SSLParameters getSupportedSSLParameters() {
            return sslFactory.getSslParameters();
        }
    
        @Override
        public X509Certificate[] getCertificateChain(String alias) {
            return sslFactory.getKeyManager()
                    .map(keyManager -> keyManager.getCertificateChain(alias))
                    .orElseThrow();
        }
    
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]);
        }
    
    }
    

    And the following dependency configuration:

    <dependencies>
        <dependency>
            <groupId>io.github.hakky54</groupId>
            <artifactId>sslcontext-kickstart</artifactId>
            <version>8.3.1</version>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.5</version>
        </dependency>
    </dependencies>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.tomcat.embed</groupId>
                <artifactId>tomcat-embed-core</artifactId>
                <version>9.0.86</version>
            </dependency>
        </dependencies>
    </dependencyManagement>