ING Tech Romania
Published in

ING Tech Romania

A simple mTLS guide for Spring Boot microservices

In a Zero trust network nothing is trusted by default. When something calls our API, how can we be sure the caller is the right one? With mutual TLS or simply mTLS, we validate parties on the other end of the connection are who they claim to be.

To understand what mTLS is and how it works we need to clarify a few things. In asymmetric cryptography we have two type of keys (public — anybody can access it /privaterestricted access) we can use to encrypt a payload. We call the output of the encryption a ciphertext. There is an important property between the two keys: if you encrypt something with one of them, you can use the other to decrypt the content.

Depending on what key we use, we obtain two things:

  • Secure transmission: the ciphertext can be decrypted only with the private key
  • Guarantee of the identity of the sender—anyone with a public key can validate the ciphertext was created with the correct private key.
https://sectigo.com/resource-library/public-key-vs-private-key

Transport Layer Security

TLS is an encryption protocol used to authenticate the server in a client-server connection and encrypt the messages between the parties to prevent others from understanding or changing them. This happens every time you access a website (if you see http://, you are exposed)

You can use your browser to see the TLS certificate, containing the public key. This is used to validate the ciphertext received from the server in the TLS handshake. If the correct private key was used to encrypt the challenge, it means the server can be trusted.

With mutual TLS the same validation happens for the client also. This means the client needs to present its TLS certificate so that the server can validate with the public key the ciphertext in the TLS handshake.

If you want more details you can check out the official specification on TLS here.

Enable TLS

How does it work in Java with Spring Boot? Well, let’s create a new project and activate TLS first. We are going to use keytool, a certificate management utility included with Java. The command below generates a file server.p12 that contains a public-private key pair valid for one year. I am running this command from src/main/resources and the file can be access from the classpath in our program.

keytool -genkeypair -alias server -keyalg RSA -keysize 4096 -validity 365 -dname "CN=Server,OU=Server,O=Examples,L=,S=CA,C=U" -keypass changeit -keystore server.p12 -storeType PKCS12 -storepass changeit

With Spring Boot we can enable TLS via configuration properties:

server.port=8443
server.ssl.key-store-type
=PKCS12
server.ssl.key-store=classpath:server.p12
server.ssl.key-store-password=changeit

Alternatively, if you are using an existing application you can pass those properties as java arguments, so you don’t have to recreate your jar again:

java -jar target/mtls-demo-0.0.1-SNAPSHOT.jar --server.ssl.key-store-type=PKCS12 --server.ssl.key-store=classpath:server.p12-server.port=8443

When we call our application using curl, we see there is a problem with our certificate. It was self-signed and by default it cannot be trusted. This is because usually a certificate authority verifies the entity applying for the digital certificate.

curl -v https://localhost:8443/hello
* Trying ::1:8443...
* Connected to localhost (::1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self signed certificate
* Closing connection 0
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Anyone can generate a self-signed certificate, but we know we are calling our application. We should be fine if we add an extra parameter to curl -k to indicate we accept an insecure connection.

*   Trying ::1:8443...
* Connected to localhost (::1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: C=U; ST=CA; L=; O=Examples; OU=Server; CN=Server
* start date: Jan 12 13:52:27 2022 GMT
* expire date: Jan 12 13:52:27 2023 GMT
* issuer: C=U; ST=CA; L=; O=Examples; OU=Server; CN=Server
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET /hello HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/7.75.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 5
<
* Connection #0 to host localhost left intact
hello

In the logs above you can see both the server and the client agree on a common cipher suite: TLS_AES_256_GCM_SHA384. You can read more details here or here. The response is exactly what we wanted because we have a simple endpoint in our example.

@RestController
public class MyController {

@GetMapping("/hello")
public String hello() {
return "hello";
}
}

Enable mTLS

The next step is to activate mutual TLS. With the properties below we tell our server it can trust clients presenting certificates from the trust store.

server.ssl.client-auth=need
server.ssl.trust-store=classpath:server-truststore.p12
server.ssl.trust-store-password=changeit

You can inspect the content with keytool -list

keytool -list -keystore server-truststore.p12
Enter keystore password:
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entryclient-public, Jan 17, 2022, trustedCertEntry,
Certificate fingerprint (SHA-256): 66:D7:F3:C9:A6:3C:C9:6E:B7:1E:38:2E:29:54:B5:1B:04:5B:0E:0E:42:74:F2:A5:4A:56:C8:BE:E1:01:95:45

Our http client (curl) needs to present its own certificate now. You can see below the two lines confirming both certificates were validated.

curl -k -v --cert-type P12 --cert client.p12:changeit https://localhost:8443/hello
* Trying ::1:8443...
* Connected to localhost (::1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
*
TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
*
TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: C=U; ST=CA; L=; O=Examples; OU=Server; CN=Server
* start date: Jan 12 13:52:27 2022 GMT
* expire date: Jan 12 13:52:27 2023 GMT
* issuer: C=U; ST=CA; L=; O=Examples; OU=Server; CN=Server
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET /hello HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/7.75.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 5
<
* Connection #0 to host localhost left intact
hello

You might encounter a few errors while working with mTLS:

  • Calling the server with an unknown certificate (not found in the trust store) returns a certificate unknown message
* TLSv1.3 (IN), TLS alert, certificate unknown (558):
* OpenSSL SSL_read: error:14094416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown, errno 0
* Closing connection 0
curl: (56) OpenSSL SSL_read: error:14094416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown, errno 0
  • Calling the server with no certificate
* TLSv1.3 (IN), TLS alert, bad certificate (554):
* OpenSSL SSL_read: error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate, errno 0
* Closing connection 0
curl: (56) OpenSSL SSL_read: error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate, errno 0

If you need more insights on how the server handles the connection, you can also add this JVM param: -Djavax.net.debug=all This adds additional logs to the server console. You can see which certificates are being exchanged or how the TLS handshake happens:

javax.net.ssl|DEBUG|26|reactor-http-nio-3|2022-01-17 15:05:42.161 EET|CertificateMessage.java:998|Produced server Certificate message (
"Certificate": {
"certificate_request_context": "",
"certificate_list": [
{
"certificate" : {
"version" : "v3",
"serial number" : "3A BF 06 B4",
"signature algorithm": "SHA384withRSA",
"issuer" : "CN=Server, OU=Server, O=Examples, L=, ST=CA, C=U",
"not before" : "2022-01-12 15:52:27.000 EET",
"not after" : "2023-01-12 15:52:27.000 EET",
"subject" : "CN=Server, OU=Server, O=Examples, L=, ST=CA, C=U",
"subject public key" : "RSA",
"extensions" : [
{
ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: BD 10 97 6A A9 69 AF 71 EF AB 41 A8 3E AF 5B 6A ...j.i.q..A.>.[j
0010: 98 BF D0 AE ....
]
]
}
]}
"extensions": {
<no extension>
}
},
]
}
)

To fake an unknown certificate alert, we can use the server certificate instead of the client one (which is available in the server-trustore.p12)

javax.net.ssl|DEBUG|27|reactor-http-nio-4|2022-01-17 15:08:58.888 EET|CertificateMessage.java:1154|Consuming client Certificate handshake message (
"Certificate": {
"certificate_request_context": "",
"certificate_list": [
{
"certificate" : {
"version" : "v3",
"serial number" : "3A BF 06 B4",
"signature algorithm": "SHA384withRSA",
"issuer" : "CN=Server, OU=Server, O=Examples, L=, ST=CA, C=U",
"not before" : "2022-01-12 15:52:27.000 EET",
"not after" : "2023-01-12 15:52:27.000 EET",
"subject" : "CN=Server, OU=Server, O=Examples, L=, ST=CA, C=U",
"subject public key" : "RSA",
"extensions" : [
{
ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: BD 10 97 6A A9 69 AF 71 EF AB 41 A8 3E AF 5B 6A ...j.i.q..A.>.[j
0010: 98 BF D0 AE ....
]
]
}
]}
"extensions": {
<no extension>
}
},
]
}
)
javax.net.ssl|ERROR|27|reactor-http-nio-4|2022-01-17 15:08:58.911 EET|TransportContext.java:313|Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (
"throwable" : {
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at ...

If you want to play around with your own certificates you can use the commands below. The key pairs are generated into server.p12 and client.p12 . We extract the public keys to client.cer and server.cer and import them into client-truststore.p12 and server-truststore.p12

keytool -genkeypair -alias server -keyalg RSA -keysize 4096 -validity 365 -dname "CN=Server,OU=Server,O=Examples,L=,S=CA,C=U" -keypass changeit -keystore server.p12 -storeType PKCS12 -storepass changeitkeytool -genkeypair -alias client -keyalg RSA -keysize 4096 -validity 365 -dname "CN=Client,OU=Server,O=Examples,L=,S=CA,C=U" -keypass changeit -keystore client.p12 -storeType PKCS12 -storepass changeit// export public keys
keytool -exportcert -alias client -file client.cer -keystore client.p12 -storepass changeit
keytool -exportcert -alias server -file server.cer -keystore server.p12 -storepass changeit//import public keys to trust stores.
keytool -importcert -keystore client-truststore.p12 -alias server-public -file server.cer -storepass changeit -noprompt
keytool -importcert -keystore server-truststore.p12 -alias client-public -file client.cer -storepass changeit -noprompt

One last thing

Don’t store your passwords as plain text in your configuration properties for production.

Ideally you should have an environment where the application retrieves its configuration properties from a versioning system. You get traceability and accountability for free when developers push configuration changes to different environments. Checkout this feature from Spring Cloud on how to create encrypted values.

server.ssl.key-store-password='{cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ'

Another simple option is to use a library that knows how to decrypt the properties when the application starts.

<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>

This means instead of changeit we can use the value below (at least for the production profile)

server.ssl.key-store-password=ENC(H6MDvmQN4JXFIePNtho/pRLuo6NUqSE4)

When the application starts, the library searches for a property: --jasypt.encryptor.password=jasyp-pass With this encryptor password our server can translate the property value back to changeit and access the content of the store.

With the example below you can encrypt and decrypt other values.

This should provide an idea on how to protect your application more with mTLS.

Thanks for reading and happy coding!

ING Tech Romania is ING Group’s global hub for technology providing over 150 services for 24 ING units globally. The services we are delivering fit into four main categories: software development, data management, non-financial risk & compliance, audit.

Recommended from Medium

Java 8 Date & Time APIs

Key takeaways from Engineering Leaders round tables

Static libraries — C programming language

3 steps to improve your IT Strategy

Writing Slack Command APIs in Ruby — Part 2

Correlation Analysis with Python

Working with Outsourcing and Externals Using Favro Custom Fields

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mihaita Tinta

Mihaita Tinta

A new kind of plumber working with Java, Spring, Kubernetes. Follow me to receive practical coding examples.

More from Medium

Configuring Vault with Spring Boot

Prometheus Monitoring Using Spring Boot

How to Use Spring Cloud Gateway to Dynamically Discover Microservices

Client application consuming microservice

Spring Security and Keycloak Integration in Spring Boot