In this new entry from Refactorizando about Mutual TLS Communication in Spring Boot, we will communicate two services with self-signed certificates using a WebClient with SSL to establish communication. Both the client-side and server-side will have certificates, and both must be valid to communicate.
Securing applications and communication between them is becoming increasingly important, and the use of certificates to enable secure communication is vital for encrypting the data.
What is Mutual TLS Communication?
Typically, we are familiar with one-way communication, where the server presents its certificate, and the client adds this certificate to the list of trusted certificates. This is the most common use case, for example, when browsing the internet, our browser (the client) may have stored certificates.
On the other hand, the topic of this article (Mutual TLS Communication in Spring Boot) is mutual communication, where both the client and the server have to trust each other. Therefore, both the client and server certificates are related. In this form of authentication, the server presents a certificate to the client, and the client presents a certificate to the server so that both are authenticated.
The following diagram explains mutual authentication between server and client:
Creating Certificates for Mutual TLS
Next, we will create the necessary certificates to establish mutual communication between the client and server. These certificates will be self-signed for testing environments, but for production environments, it is better to use certificates signed by a Certificate Authority (CA).
Before creating the client and server certificates, we will create the Java Keystore (JKS):
keytool -v -genkeypair -dname "CN=Refactorizando,OU=Madrid,O=Refactorizando,C=SP" -keystore identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias server -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -ext SubjectAlternativeName:c=DNS:localhost,IP:127.0.0.1
Creating the Server Certificate
Next, we will export the server certificate:
keytool -v -exportcert -file server.cer -alias server -keystore identity.jks -storepass secret -rfc
Once we have generated the server certificate, we will create a truststore for the client and add the certificate to this truststore:
keytool -v -importcert -file server.cer -alias server -keystore truststore.jks -storepass secret -noprompt
Creating the Client Certificate
Now that we have the server certificate and the client’s truststore containing the server’s certificate, we will create the client certificate to establish communication, and the server will trust the client:
keytool -v -genkeypair -dname "CN=Refactorizando,OU=Madrid,O=Refactorizando,C=SP" -keystore identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias client -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -ext SubjectAlternativeName:c=DNS:localhost,IP:127.0.0.1
Next, we will export the client certificate to be added to the server’s truststore:
keytool -v -exportcert -file client.cer -alias client -keystore identity.jks -storepass secret -rfc
After exporting the client certificate, we still need to add it to the server’s truststore:
keytool -v -importcert -file client.cer -alias client -keystore truststore.jks -storepass secret -noprompt
Once we have completed all these steps, it is time to establish communication between our client and server applications.
Mutual TLS Communication and WebClient in Spring Boot
Now that we have completed all the previous steps and have the truststore and key store, we will create two Spring Boot applications to establish communication through WebClient.
Maven Dependencies for Mutual TLS Communication in Spring Boot
Server Dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
The above dependency is essential for the server.
Client Dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency>
The four dependencies above are essential for communicating through WebClient with the server.
Creating the Server
Next, we will create a very simple service with Spring Boot, which will have the previously created client certificate and will be called through an endpoint.
Adding Keystore and Truststore to the Server’s Classpath
Configuration of application.yaml for TLS Communication
In the above image, we can see the necessary configuration to enable the server’s SSL communication.
SSL Configuration:
- enabled: To activate or deactivate SSL communication.
- client-auth: Client authentication is mandatory.
- key-store: Path to the keystore.
- key-store-password: Keystore password.
- key-alias: Alias set.
- key-store-type: Keystore type.
- key-store-provider: Keystore provider.
- trust-store: Truststore path.
- trust-store-password: Truststore password.
- trust-store-type: Truststore type.
Creating the Endpoint
We will create a simple endpoint that will be invoked by the client, which will simply return a String.
@RestController public class ServiceVerificationResource { @GetMapping("certificate-verify") public ResponseEntity<String> tlsServiceVerification() { return ResponseEntity.ok("verified"); } }
Creating the Client
As we have seen, the server configuration is quite simple, as Spring will do most of the work for us. The client configuration requires a bit more implementation, as we will add a WebClient with security.
The first step will be to configure the WebClient to allow communication over TLS.
WebClient Configuration with TLS/SSL
To create a WebClient with TLS, we will create a @Bean during service startup so that it can be invoked when needed.
@Configuration public class ServiceRegistryConfig { @Value("${server.base-url}") private String baseUrl; @Value("${server.endpoint}") private String endpoint; @Bean public WebClient configureWebclient(@Value("${server.ssl.trust-store}") String trustStorePath, @Value("${server.ssl.trust-store-password}") String trustStorePass, @Value("${server.ssl.key-store}") String keyStorePath, @Value("${server.ssl.key-store-password}") String keyStorePass, @Value("${server.ssl.key-alias}") String keyAlias) { SslContext sslContext; final PrivateKey privateKey; final X509Certificate[] certificates; try { final KeyStore trustStore; final KeyStore keyStore; trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray()); keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray()); List<Certificate> certificateList = Collections.list(trustStore.aliases()).stream() .filter( t -> { try { return trustStore.isCertificateEntry(t); } catch (KeyStoreException exception) { throw new RuntimeException("Error reading truststore", exception); } }) .map( t -> { try { return trustStore.getCertificate(t); } catch (KeyStoreException exception) { throw new RuntimeException("Error reading truststore", exception); } }) .collect(Collectors.toList()); certificates = certificateList.toArray(new X509Certificate[certificateList.size()]); privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()); Certificate[] certChain = keyStore.getCertificateChain(keyAlias); X509Certificate[] x509CertificateChain = Arrays.stream(certChain) .map(certificate -> (X509Certificate) certificate) .collect(Collectors.toList()) .toArray(new X509Certificate[certChain.length]); X509Certificate certificate = x509CertificateChain[0]; validateCertificate(certificate); sslContext = SslContextBuilder.forClient() .keyManager(privateKey, keyStorePass, x509CertificateChain) .trustManager(certificates) .build(); HttpClient httpClient = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)); return webClientConfiguration(httpClient); } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | UnrecoverableKeyException e) { throw new RuntimeException(e); } } private boolean validateCertificate(X509Certificate certificate) { var certificateExpirationDate = certificate.getNotAfter().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); var certificateStartDate = certificate.getNotBefore().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); if (LocalDate.now().isAfter(certificateExpirationDate)) { throw new ServiceExpirationDateException("Service date expiration"); } if (LocalDate.now().isBefore(certificateStartDate)) { throw new ServiceStartDateException( "Service cannot be used until " + certificateStartDate.toString()); } var subject = Arrays.stream(certificate.getSubjectDN().getName().split(",")) .map(i -> i.split("=")) .collect(Collectors.toMap(element -> element[0].trim(), element -> element[1].trim())); if (!subject.get("O").equalsIgnoreCase("Refactorizando")) { throw new OrganizationNameException("Organization is not correct"); } return true; } private WebClient webClientConfiguration(HttpClient httpClient) { ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); var webClient = WebClient.builder() .clientConnector(connector) .baseUrl(baseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); var reponse = webClient.get().uri(endpoint).retrieve().bodyToMono(String.class).block(); assert Objects.requireNonNull(reponse).equalsIgnoreCase("verified"); return webClient; } }
In the ServiceRegistryConfig class, we will perform several checks on the certificate and add security to the WebClient.
To perform checks on the certificate, we will use the X509Certificate object, which provides us with an API with several methods to perform checks on the certificate, such as:
- NotAfter(): Method to view the certificate’s expiration date.
- NotBefore(): Method to see when the certificate will be valid from.
- SubjectDN(): Retrieves the subject of the certificate.
To secure our WebClient, we will create an SslContext, to which we add the keyManager using the privateKey, keyStorePass, and the certificates that we have in the classpath as an array of X509Certificate objects. Additionally, we add the trustManager and pass it the list of certificates we have generated.
Possible TLS Communication Errors
When creating the certificates and attempting to establish point-to-point communication, if we have not generated the certificates correctly or have misconfigured something, we may encounter errors such as the following:
- No subject alternative names present: This error generally occurs when we have not configured the SubjectAlternativeName.
- No subject alternative names matching IP… : The IP is not included in the SubjectAlternativeName.
- PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find a valid certification path to the requested target: Certificate signed by another authority than the one found in the truststore.
- Software caused connection abort: recv failed: This indicates that the server does not trust the client, usually due to either the client not having a certificate or the client’s certificate being incorrect.
- Received fatal alert: handshake_failure: Both the client and server must encrypt using the same cipher; if not, this error will occur
Conclusion
In this entry on Mutual TLS Communication in Spring Boot, we have demonstrated how easy it is to generate secure communication using self-signed certificates.
The increasing need to enhance security in communications makes it more and more necessary to implement secure communications, such as using the Spring WebClient and SSLContext.
If you wish, you can see the examples in action on our Github.
Ensuring secure communication is essential for protecting sensitive information and ensuring the integrity of data exchange between services. The use of Mutual TLS (Transport Layer Security) in Spring Boot provides a robust mechanism for authenticating both the client and server, ensuring that only trusted parties can communicate securely. By following the steps outlined in this article, you can establish secure communication between your Spring Boot applications and enhance the overall security of your system.