How to update Truststore dynamically?
Asked Answered
J

4

13

I have currently implemented mutual TLS in my Spring Boot application and I am doing it programmatically, like so:

@Bean
public ServletWebServerFactory servContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    TomcatConnectorCustomizer tomcatConnectorCustomizer = new TomcatConnectorCustomizer() {
        @Override
        public void customize(Connector connector) {
            connector.setPort(8443);
            connector.setScheme("https");
            connector.setSecure(true);
            Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();

            protocol.setSSLEnabled(true);
            protocol.setKeystoreType("PKCS12");
            protocol.setKeystoreFile(keystorePath);
            protocol.setKeystorePass(keystorePass);
            //client must be authenticated (the cert he sends should be in our trust store)
            protocol.setSSLVerifyClient(Boolean.toString(true));
            protocol.setTruststoreFile(truststorePath);
            protocol.setTruststorePass(truststorePass);
            protocol.setKeyAlias("APP");
        }
    };
    tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
    return tomcat;
}

This is working fine and as expected, but I have a requirement where I need to update the trust store during runtime (for example, when a @getmapping endpoint is invoked).

Specifically, I need to add a new certificate to the TrustStore without stopping/restarting the application. So I will have to somehow modify the in-memory trust store of my application.

How can I do this?

I tried to add a bean dynamically which adds a new Trust Manager to the SslContext, but this does not work.

@GetMapping("/register")
public String Register() throws Exception {
    ConfigurableApplicationContext configContext = (ConfigurableApplicationContext) appContext;
    ConfigurableListableBeanFactory beanRegistry = configContext.getBeanFactory();
    SSLContext sslContext = getSSLContext();
    beanRegistry.registerSingleton("sslContext", sslContext);
    return "okay";
}


public  SSLContext getSSLContext() throws Exception {
    TrustManager[] trustManagers = new TrustManager[] {
            new ReloadableX509TrustManager(truststoreNewPath)
    };
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    return sslContext;
}

I also tried to invoke the above getSSLContext() as a @bean, which did not work either.

My current solutions are based on these links, which are for Java, but I'm not sure how to implement them in my Spring application.

I have found a solution which describes exactly how to have a dynamic trust store, but I am not able to figure out how to reload the trust store during runtime. Say, for example, when a GET endpoint is invoked.

Client Certificate authentication without local truststore I have a list of Certificates, I just need to know how to invoke the ReloadableX509TrustManager's addCertificates() method.

Jocosity answered 22/7, 2019 at 10:9 Comment(5)
but this does not work because of... ? Which did not work too. because of ... (bis) ??Hyoscyamine
@Hyoscyamine I honestly don't know.Jocosity
It seams that artice you posted covers the topic. ReloadableX509TrustManager is perfect candidate to become a bean and use it to create context like aboveHyoscyamine
@Hyoscyamine I really don't know how to proceed after this. I've tried many thingsJocosity
@Hyoscyamine Could you please show me how?Jocosity
H
2

First, make your ReloadableX509TrustManager a managed bean - eg annotate with @Component

@Component
class ReloadableX509TrustManager 
    implements X509TrustManager {
.....
    public ReloadableX509TrustManager(@Value("someValueFromAppConfig")String tspath){....}
.....

Second, use it in your controller instead of creating new one eg

@GetMapping("/register")
public String Register() throws Exception {
    ConfigurableApplicationContext configContext = (ConfigurableApplicationContext) appContext;
    ConfigurableListableBeanFactory beanRegistry = configContext.getBeanFactory();
    SSLContext sslContext = getSSLContext();
    beanRegistry.registerSingleton("sslContext", sslContext);
    return "okay";
}

@Autowired private ReloadableX509TrustManager reloadableManager;
public  SSLContext getSSLContext() throws Exception {
    TrustManager[] trustManagers = new TrustManager[] {
            reloadableManager
    };
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    return sslContext;
}

Thrid, follow article to get to know how to "reload" that trust manager. It can be done by changing most of methods to package protected and invoke it from some sort of sertificate service - or make it public and call directly. The choice is yours.

Hyoscyamine answered 23/7, 2019 at 11:13 Comment(5)
I will try this and get back to you.Jocosity
I tried your above method. It runs without error but the Spring trust store is still not updated?. I think when we register the new bean "SSLContext" it is not getting picked up by the applicationJocosity
I think I'm doing that, I'm calling reloadableManager.reloadTrustManager(); at the beginning of the getSSLContext() method.Jocosity
and for this solution to work, the spring should expose a SslContext bean to begin with right?Jocosity
Making the ReloadableManager prototype or request scoped can helpBeaulahbeaulieu
S
0

I have tried this before with Spring Boot and the embedded Tomcat server but without any luck. I did found a way to accomplish it with Spring Boot and Jetty server. Tomcat can only be configured with properties. The library does not expose the underlying sslcontext and therefor it is not possible to manipulate and for this use case I could not found a way for tomcat. Jetty allows to pass a sslcontext which you can manipulate.

So I have created a custom TrustManager which is just a wrapper for the actual TrustManager. This custom TrustManager can delegate the incoming method calls to the actual trustmanager. However this custom trustmanager has a additional capability of changing the actual trustmanager whenever you want it, for example when you want to update the truststore.

I have created a library for this use case, so it might be handy but you can accomplish the same without the library.

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart-for-jetty</artifactId>
    <version>7.4.4</version>
</dependency>

Usage

SSLFactory baseSslFactory = SSLFactory.builder()
          .withDummyIdentityMaterial()
          .withDummyTrustMaterial()
          .withSwappableIdentityMaterial()
          .withSwappableTrustMaterial()
          .build();

Runnable sslUpdater = () -> {
    SSLFactory updatedSslFactory = SSLFactory.builder()
          .withIdentityMaterial(Paths.get("/path/to/your/identity.jks"), "password".toCharArray())
          .withTrustMaterial(Paths.get("/path/to/your/truststore.jks"), "password".toCharArray())
          .build();
    
    SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
};

// initial update of ssl material to replace the dummies
sslUpdater.run();
   
// update ssl material every hour    
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(sslUpdater, 1, 1, TimeUnit.HOURS);

This project demonstrates exactly this use case, see here: GitHub - Instant server ssl reloading. The project demonstrates two ways of updating the ssl material:

  • Through a rest controller: AdminController
    • This makes it possible to update the ssl material from an external api call
  • Through a file listener: FileBasedSslUpdateService
    • This makes it possible to refresh the trust material every time when the trust store file gets updated

You can find the library here: GitHub - SSLContext Kickstart

Below is the full server configuration:

This example can be easily used for option 1 and option 2

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
</dependency>
<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>

SSLConfig class

@Configuration
public class SSLConfig {

    @Bean
    public SSLFactory sslFactory(ApplicationProperty applicationProperty) {
        return SSLFactory baseSslFactory = SSLFactory.builder()
                .withDummyIdentityMaterial()
                .withDummyTrustMaterial()
                .withSwappableIdentityMaterial()
                .withSwappableTrustMaterial()
                .build();
    }

    @Bean
    public SslContextFactory.Server sslContextFactory(SSLFactory sslFactory) {
        return JettySslUtils.forServer(sslFactory);
    }
}

Server configuration class

@Configuration
public class ServerConfig {

    @Bean
    public ConfigurableServletWebServerFactory webServerFactory(SslContextFactory.Server sslContextFactory) {
        JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
    
        JettyServerCustomizer jettyServerCustomizer = server -> {
            ServerConnector serverConnector = new ServerConnector(server, sslContextFactory);
            serverConnector.setPort(8443);
            server.setConnectors(new Connector[]{serverConnector});
        };
        factory.setServerCustomizers(Collections.singletonList(jettyServerCustomizer));
    
        return factory;
    }

}

And the service which updates the ssl material


import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.SSLFactoryUtils;
import org.springframework.stereotype.Service;

import java.nio.file.Path;

@Service
public class FileBasedSslUpdateService {

    private final SSLFactory baseSslFactory;

    public FileBasedSslUpdateService(SSLFactory baseSslFactory) {
        this.baseSslFactory = baseSslFactory;
    }

    public void update() {
        SSLFactory updatedSslFactory = SSLFactory.builder()
                .withIdentityMaterial(Path.of("/path/to/your/identity.jks"), "secret".toCharArray())
                .withTrustMaterial(Path.of("/path/to/your/truststore.jks"), "secret".toCharArray())
                .build();

        SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
    }
    
}

A more advanced setup of the FileBasedSslSupdateService can be found here: FileBasedSslUpdateService Next to that you can also update your ssl configuration supplied through a rest controller like the following example: AdminController

Superlative answered 24/10, 2021 at 11:20 Comment(0)
Z
-1

Update 2 :

My current solutions are based on these links, which are for Java but I'm not sure how to implement them in my Spring Application.

https://jcalcote.wordpress.com/2010/06/22/managing-a-dynamic-java-trust-store/

Spring or not, your main goal doesn't require constraints to the applicative technology in a broad sense, the important thing is that your application (Client) it will be able to load new certificates into the client at any point during runtime and make changes on-the-fly. Let's say that to make what has been said, we could implement our logic in one of our class (TrustManager) implementing X509TrustManager.

Implementing the trust manager in a Client

The goal here is to tell the runtime that you want to use this new class to verify certificates. We have to instantiate a new SSLContext with our TrustManager and use this to specify a new SSLSocketFactory.

try {
    String trustStorePath = "path to a truststore that you have";
    String trustStorePassword = "password of trustStore";
    String defaultTrustStore = "path to default truststore";

    // Initialize the new trustManager with the default trust store
    TrustManager trustManager = new TrustManager(defaultTrustStore);

    // Load the new Keystore and decrypt it
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(new FileInputStream(trustStorePath), trustStorePassword.toCharArray());

    // Add all of the certficates in the truststore and add them to the trust manager
    Enumeration<String> enumerator = ks.aliases();
    ArrayList<Certificate> certs = new ArrayList<>();
    while (enumerator.hasMoreElements()) {
        String currentAlias = enumerator.nextElement();
        certs.add(ks.getCertificate(currentAlias));
    }
    trustManager.addCertificates(certs);

    // Initialize the SSLContext and add it to the client conduit.
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, new TrustManager[] {trustManager}, null);

    // Set the new TrustManager in the client.
    HTTPConduit httpConduit = (HTTPConduit) ClientProxy.getClient(service).getConduit();
    TLSClientParameters tlsCP = new TLSClientParameters(); 
    httpConduit.setTlsClientParameters(tlsCP);
    tlsCP.setSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
    e.printStackTrace();
}

If you need of a reference, here you'll find it.

Zeidman answered 24/7, 2019 at 13:11 Comment(2)
I am not trying to implement a trust manager in a client. I want to modify/update the trust manager of Spring's tomcat server during run time.Jocosity
Once you have your trust manager, what's stopping you to configure Spring to use that trustmanager?Zeidman
E
-1

I also came through the same scenario where I have to update the TrustManager of my spring Boot App on the fly.

  1. I uploaded the certificate to my default cacert file(i.e Default SSL Context TrustManager) - This will take care of the connections establishing through default SSLContext.
  2. But I also have two different REST Template Beans, One for establishing 1way/Mutual SSL Connection and another for 2Way SSL Connection. Since these Beans got initialized during the startup
    • changing the default TrustManager will not update the TrustManager of REST Template.
    • For this, I updated the RestTemplate Bean with the new Request Factory dynamically which solved my issue.
Ewell answered 2/8, 2019 at 21:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.