
Problem
We want to be able to exchange HTTP requests and responses with our application over an encrypted connection.
HTTPS and SSL
SSL (Secure Sockets Layer) is a standard for secure communication over the transport layer. It defines a set of protocols and algorithms via which a client can establish an encrypted communication channel to a server by a process called SSL Handshake. The SSL standard has been superseded by a newer specification called TLS (Transport Layer Security). For the rest of this text we will use the terms SSL and TLS interchangeably, as is common in a large part of the literature.
HTTPS (Hypertext Transfer Protocol Secure) is an updated version of the HTTP protocol that works over an SSL connection.
The following sections describe how to enable a Spring Boot application to receive requests and provide responses over SSL.
Server setup
- First, we will create a a file called keystore which will contain the public key, public key certificate and private key for the server.
- Choose a strong password to use to encrypt the keystore file.
- Store the keystore password in some secret management service, e.g. AWS Secrets Manager. In the following steps, we asume you are using AWS Secrets Manager to store the secrets.
- If you haven’t added the AWS Secrets Manager Java SDK to your project, do it now following the steps at Working with AWS Secrets Manager .
- Create a bean in your service that, in its initialization, connects to the secret management service and retrieves the password. You can find an example of how to do this in Working with AWS Secrets Manager .
- Create the keystore file with this command:
keytool -genkeypair -alias <key_store_alias> -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore <key_store_file_name>.p12 -validity 3650
When it asks for the name of the server, enter localhost:
What is your first and last name?
[Unknown]: localhost
This is important in order to be able to connect to the server using the hostname localhost, otherwise the hostname verification (which is part of HTTPS) will fail. Note: supposedly there is a way in Spring Webflux to disable hostname verification programmatically (see this StackOverflow question). However when I tried it, it didn’t work because the matches() method was never called. If you find a way to disable hostname verification in Webflux that works, let me know.
If you run the keytool command more than once to regenerate the keys, keep in mind that you will need to rebuild the server’s jar every time in order for the change to take effect.
- Export the server’s public key certificate from the keystore file, so that it can be added in the client’s CA collection:
$ keytool -exportcert -rfc -keystore <key_store_file_name>.p12 -storetype PKCS12 -alias <key_store_alias> -file <server_public_key_file_name>-pub.crt
- Add the keystore file to the application’s repo, in a suitable subdirectory within the resources folder.
- Use the following snippet to create the ServerProperties bean with a factory method, and inside this method, set the keystore password using the bean that reads it from the secret management service (see Working with AWS Secrets Manager for a tutorial on how to create the bean the reads the keystore password from your secret management service):
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.web.server.Ssl;
@Configuration
public class SSLConfig {
@Autowired
private KeyStorePasswordRetriever keyStorePasswordRetriever;
@Bean
public ServerProperties serverProperties() {
Ssl ssl = new Ssl();
ssl.setKeyStorePassword(keyStorePasswordRetriever.getKeyStorePassword());
ServerProperties serverProperties = new ServerProperties();
serverProperties.setSsl(ssl);
return serverProperties;
}
}
- Add the following to your application.properties:
# The format used for the keystore. It could be set to JKS in case it is a JKS file
server.ssl.key-store-type=PKCS12
# The path to the keystore containing the certificate e.g. classpath:com/sgonzalez/myservice/myservice-ssl-keystore.p12
server.ssl.key-store=classpath:<path_to_your_keystore.p12_file_in_your_classpath>
# The alias mapped to the certificate
server.ssl.key-alias=<myservice-ssl-keystore.p12>
# Configure Spring Security to accept only HTTPS requests:
server.ssl.enabled=true
Client setup
For every client that needs to connect to the application, do the following (we assume here that the client is also a Spring-based Java application):
- Add this in the application.properties:
# The server's hostname (needs to match the name entered when creating the server's keystore)
serverHost=localhost
# The server's port number
serverPort=9000
# Connection timeout
serverConnectionTimeoutMillis=200
# TCP read timeout
serverReadTimeoutMillis=2400
# TCP write timeout
serverWriteTimeoutMillis=400
- Configure the WebClient bean in the following way (this assumes the client is a Java Spring Boot application using Spring Webflux and Reactor Netty as container):
@Configuration
class MyClientConfiguration {
@Value("${serverHost}")
private String host;
@Value("${serverPort}")
private int port;
@Value("${serverConnectionTimeoutMillis}")
private int connectionTimeoutMillis;
@Value("${serverReadTimeoutMillis}")
private int readTimeoutMillis;
@Value("${serverWriteTimeoutMillis}")
private int writeTimeoutMillis;
@Bean
WebClient myWebClient() throws IOException {
SslContext context = SslContextBuilder
.forClient()
.trustManager(new ClassPathResource("<public_key_certificate_file_name>-pub.crt", this.getClass()).getInputStream())
.build();
TcpClient tcpClient = TcpClient
.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis)
.doOnConnected(connection -> {
connection.addHandlerLast(new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS));
connection.addHandlerLast(new WriteTimeoutHandler(writeTimeoutMillis, TimeUnit.MILLISECONDS));
});
HttpClient httpClient = HttpClient
.from(tcpClient)
.secure(sslContextSpec -> sslContextSpec
.sslContext(context));
return WebClient
.builder()
.baseUrl(String.format("https://%s:%d", host, port))
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
Troubleshooting notes
- To attempt an SSL handshake to the locally running server (shows all certificates involved):
$ openssl s_client -connect localhost:9000
- To check a certificate file in PEM format
$ openssl x509 -in myservice-pub.crt -text -noout
- To check a certificate file in P12 format (you’ll have to enter the keystore password)
openssl pkcs12 -info -in keyStore.p12
Sources
- On SSL:
- Tutorial on setting up connection to a server with a self-signed certificate:
- On configuring the SSL in the client with Reactor Netty:
- Spring Boot’s documentation about SSL configuration:
- On how to set the keystore password programmatically:
- On how to configure SSL in Spring WebClient:
When setting up HTTPS in a Spring Boot application using a keystore stored in AWS Secrets Manager, what are some best practices to handle automatic certificate renewal without needing to rebuild the server JAR each time? Also, is there a recommended way to manage hostname verification in Spring Webflux clients beyond the standard localhost setup?
LikeLike
If you wanted you could renew the keystore file without having to rebuild the jar, by putting the keystore file in Secrets Manager along with its password. But you would need to consider whether this is worthwhile, keep in mind that certificate renewal is an infrequent task that you do like once every five years.
You wouldn’t use localhost as the hostname. In real production code you would set the hostname to the actual hostname of the server you deploy the application on. The example code on this blog post uses localhost as an example.
LikeLike