Easily Update and Reload SSL for a Server and an HTTP Client
In this tutorial, learn how to update and reload your SSL configuration whenever needed without restarting your server or recreating your HTTP client.
Join the DZone community and get the full member experience.
Join For FreeThis tutorial walks through the process of configuring your server or HTTP client to enable hot reloading of the SSL configuration at runtime. This will result in no longer restarting your server when the certificates need to be updated, and you won't need to recreate your HTTP client when you want to use your new certificates. In this tutorial, we will cover only a Spring Boot application with Jetty as an embedded server to demonstrate the basic configuration and the different ways to trigger an update. However, every server or HTTP client which uses a SSLContext
, SSLServerSocketFactory
/SSLSocketFactory
, TrustManager
or KeyManager
to configure SSL can also enable hot reloading, including Scala and Kotlin-based servers and HTTP clients.
The hot reloading mechanism is provided by the SSLContext Kickstart library and all of the code examples shown in this tutorial can also be found on GitHub: Java Tutorials.
This tutorial will cover the following topics:
- Required dependencies
- Server configuration
- Reloading examples
- Basic SSL reloading
- File-based SSL reloading
- Database-based SSL reloading
- Endpoint/Resource/Controller-based SSL reloading
- Demo video
- Tested servers
- Compatible HTTP Clients
Dependencies
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-jetty</artifactId>
</dependency>
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart-for-pem</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Examples
Server Configuration
The server will be initially configured with a keystore and trust store from the classpath which will also be swappable. The swappable option will enable the hot reloading feature.
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.JettySslUtils;
import org.eclipse.jetty.util.ssl.SslContextFactory;
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) {
return SSLFactory.builder()
.withIdentityMaterial(keyStorePath, keyStorePassword)
.withTrustMaterial(trustStorePath, trustStorePassword)
.withSwappableIdentityMaterial()
.withSwappableTrustMaterial()
.build();
}
@Bean
public SslContextFactory.Server sslContextFactory(SSLFactory sslFactory) {
return JettySslUtils.forServer(sslFactory);
}
}
The SSL configuration needs to be injected into the Jetty web server, which is done as with the example below:
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
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 org.springframework.context.annotation.Configuration;
import java.util.Collections;
@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;
}
}
Basic SSL Reloading
This part will demonstrate the easiest way to reload the SSL configuration and also demonstrates what is needed to get you started. First of all, you need a new instance of SSLFactory
which is constructed with the new/updated keystores as shown below.
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);
The base SSLFactory
is the initial SSL configuration which was created with the swappable options. The base and the new SSLFactory
need to be passed to the SSLFactoryUtils
to reload the SSL configuration. The cache will also be cleared so a new SSL handshake will be initialized.
File-Based
The file-based SSL update service will validate every 10 seconds if the identity and trust store files have been updated. If that is the case, it will get the content and create a new SSLFactory
out of it and update the base SSL configuration. The job is configured to check every 10 seconds, but this can be adjusted to your own needs with a cron
statement.
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.SSLFactoryUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@Service
public class FileBasedSslUpdateService {
private static final Logger LOGGER = LoggerFactory.getLogger(FileBasedSslUpdateService.class);
private static final Path identityPath = Path.of("/path/to/your/identity.jks");
private static final Path trustStorePath = Path.of("/path/to/your/truststore.jks");
private static final char[] identityPassword = "secret".toCharArray();
private static final char[] trustStorePassword = "secret".toCharArray();
private ZonedDateTime lastModifiedTimeIdentityStore = ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
private ZonedDateTime lastModifiedTimeTrustStore = ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
private final SSLFactory baseSslFactory;
public FileBasedSslUpdateService(SSLFactory baseSslFactory) {
this.baseSslFactory = baseSslFactory;
}
/**
* Checks every 10 seconds if the keystore files have been updated.
* If the files have been updated the service will read the content and update the ssl material
* within the existing ssl configuration.
*/
@Scheduled(cron = "*/10 * * * * *")
private void updateSslMaterial() throws IOException {
if (Files.exists(identityPath) && Files.exists(trustStorePath)) {
BasicFileAttributes identityAttributes = Files.readAttributes(identityPath, BasicFileAttributes.class);
BasicFileAttributes trustStoreAttributes = Files.readAttributes(trustStorePath, BasicFileAttributes.class);
boolean identityUpdated = lastModifiedTimeIdentityStore.isBefore(ZonedDateTime.ofInstant(identityAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC));
boolean trustStoreUpdated = lastModifiedTimeTrustStore.isBefore(ZonedDateTime.ofInstant(trustStoreAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC));
if (identityUpdated && trustStoreUpdated) {
LOGGER.info("Keystore files have been changed. Trying to read the file content and preparing to update the ssl material");
SSLFactory updatedSslFactory = SSLFactory.builder()
.withIdentityMaterial(identityPath, identityPassword)
.withTrustMaterial(trustStorePath, trustStorePassword)
.build();
SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
lastModifiedTimeIdentityStore = ZonedDateTime.ofInstant(identityAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC);
lastModifiedTimeTrustStore = ZonedDateTime.ofInstant(trustStoreAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC);
LOGGER.info("Updating ssl material finished");
}
}
}
}
Database-Based
This example will use the database as a source for getting the SSL material. The previous example used keystore files; however, the database will use PEM formatted strings. First, we need a data class:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.sql.Timestamp;
@Entity
@Table(name = "SSL_MATERIAL")
public class SSLMaterial {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private Timestamp updatedAt;
private String identityContent;
private String identityPassword;
private String trustedCertificates;
// Getters and Setters
}
We also need a JPA repository to get the row from the database as a Java object. This can be done with the following snippet:
import nl.altindag.server.model.SSLMaterial;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SSLMaterialRepository extends JpaRepository<SSLMaterial, Long> {
}
The database-based SSL update service will validate every 10 seconds if the identity and trusted certificates have been updated. If that is the case, it will get the content and create a new SSLFactory
out of it and update the base SSL configuration. The job is configured to check every 10 seconds, but this can be adjusted to your own needs with a cron
statement.
import nl.altindag.server.model.SSLMaterial;
import nl.altindag.server.repository.SSLMaterialRepository;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.PemUtils;
import nl.altindag.ssl.util.SSLFactoryUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@Service
public class DatabaseBasedSslUpdateService {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseBasedSslUpdateService.class);
private final SSLFactory baseSslFactory;
private final SSLMaterialRepository sslMaterialRepository;
private ZonedDateTime lastModifiedTime = ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
public DatabaseBasedSslUpdateService(SSLFactory baseSslFactory, SSLMaterialRepository sslMaterialRepository) {
this.baseSslFactory = baseSslFactory;
this.sslMaterialRepository = sslMaterialRepository;
}
/**
* This setup is very basic, and therefore currently does not validate if the content on the database has been updated.
* Fetches every 10 seconds the ssl material from the database to update the server.
*/
@Scheduled(cron = "*/10 * * * * *")
private void updateSslMaterial() {
LOGGER.info("Fetching ssl material...");
SSLMaterial sslMaterial = sslMaterialRepository.findById(1L)
.orElseThrow();
ZonedDateTime sslMaterialUpdatedAt = ZonedDateTime.ofInstant(sslMaterial.getUpdatedAt().toInstant(), ZoneOffset.UTC);
if(sslMaterialUpdatedAt.isBefore(lastModifiedTime) || sslMaterialUpdatedAt.isEqual(lastModifiedTime)) {
LOGGER.info("No changes detected. Skipping of refreshing the ssl configuration");
return;
}
LOGGER.info("Changes detected. Starting to update ssl material and refreshing the ssl configuration");
X509ExtendedKeyManager keyManager = PemUtils.parseIdentityMaterial(sslMaterial.getIdentityContent(), sslMaterial.getIdentityPassword().toCharArray());
X509ExtendedTrustManager trustManager = PemUtils.parseTrustMaterial(sslMaterial.getTrustedCertificates());
SSLFactory updatedSslFactory = SSLFactory.builder()
.withIdentityMaterial(keyManager)
.withTrustMaterial(trustManager)
.build();
SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
lastModifiedTime = sslMaterialUpdatedAt;
LOGGER.info("Finished updating ssl material and refreshing the ssl configuration");
}
}
Endpoint/Resource/Controller-Based
The endpoint-based SSL update service makes it possible to supply the new SSL material from an HTTP Post request. The new keystore files can be sent as a byte array within the SSLUpdateRequest
model. The server can consume that request by constructing an in-memory keystore object and using that to create an SSLFactory
.
First, we need an SSLUpdateRequest
model as shown below:
public class SSLUpdateRequest {
private byte[] keyStore;
private char[] keyStorePassword;
private byte[] trustStore;
private char[] trustStorePassword;
public SSLUpdateRequest() {}
public SSLUpdateRequest(byte[] keyStore, char[] keyStorePassword, byte[] trustStore, char[] trustStorePassword) {
this.keyStore = keyStore;
this.keyStorePassword = keyStorePassword;
this.trustStore = trustStore;
this.trustStorePassword = trustStorePassword;
}
// Getters and Setters
}
Next, we need to create an endpoint. In this case, it will be https://localhost:8443/admin/ssl, which consumes a JSON request of the SSLUpdateRequest
model. Below is an example of the actual endpoint which takes SSLUpdateRequest
and maps it into an SSLFactory
and reloads the SSL configuration.
import nl.altindag.server.model.SSLUpdateRequest;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.SSLFactoryUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@RestController
public class AdminController {
private final SSLFactory baseSslFactory;
public AdminController(SSLFactory baseSslFactory) {
this.baseSslFactory = baseSslFactory;
}
@PostMapping(value = "/admin/ssl", consumes = MediaType.APPLICATION_JSON_VALUE)
public void updateKeyManager(@RequestBody SSLUpdateRequest request) throws IOException {
try (InputStream keyStoreStream = new ByteArrayInputStream(request.getKeyStore());
InputStream trustStoreStream = new ByteArrayInputStream(request.getTrustStore())) {
SSLFactory updatedSslFactory = SSLFactory.builder()
.withIdentityMaterial(keyStoreStream, request.getKeyStorePassword())
.withTrustMaterial(trustStoreStream, request.getTrustStorePassword())
.build();
SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
}
}
}
Demo
Tested Servers
- Spring Boot
- gRPC
- Vert.x
- Netty
The above list of servers have been tested and proven to be working. Other Java, Kotlin, or Scala-based servers which accept an SSLContext
, KeyManager
, TrustManager
, or SSLServerSocketFactory
will also work. All of the source code for the different examples and different servers listed above is available at the Java Tutorial link on GitHub given earlier in this article. You can follow the steps within the README file to try it out yourself locally.
Compatible HTTP Clients
The above list of HTTP clients is not tested on this functionality of hot reloading SSL at runtime; however, it will work as it is fully compatible with the library. The list of supported HTTP clients for Java, Kotlin, and Scala are as follows:
Java
- Apache HttpClient
- Apache HttpAsyncClient
- Apache 5 HttpClient
- Apache 5 HttpAsyncClient
- JDK HttpClient
- Old JDK HttpClient
- Netty Reactor
- Jetty Reactive HttpClient
- Spring RestTemplate
- Spring WebFlux WebClient Netty
- Spring WebFlux WebClient Jetty
- OkHttp
- Jersey Client
- Old Jersey Client
- Apache CXF JAX-RS
- Apache CXF using ConduitConfigurer
- Google HttpClient
- Unirest
- Retrofit
- Async Http Client
- Feign
- Methanol
- Vert.x Web Client
- gRPC
- Elasticsearch
- Jetty WebSocket
Kotlin
- Fuel
- http4k with Apache 4
- http4k with Async Apache 4
- http4k with Apache 5
- http4k with Async Apache 5
- http4k with Java Net
- http4k with Jetty
- http4k with OkHttp
- kohttp
- Ktor with Android engine
- Ktor with Apache engine
- Ktor with Java engine
- Ktor with Okhttp engine
Scala
- Twitter Finagle
- Twitter Finagle Featherbed
- Akka HTTP Client
- Dispatch Reboot
- ScalaJ/Simplified HTTP Client
- STTP
- Requests-Scala
- Http4s Blaze Client
- Http4s Java Net Client
Opinions expressed by DZone contributors are their own.
Comments