Introduction
Securing a microservice system involves several aspects. One concern that we must deal with is the data exposed by services. We want to be sure that the data is encrypted while it flows from the server to the client. Also, we must be sure that our system is made of mutually trusted services, enforced to authenticate with each other before starting the communication. SSL is a protocol that deals with these issues. This article will show how to secure Spring Cloud applications with the SSL protocol.
Securing Application Data Exposed by a Service with SSL Protocol
A common scenario is to serve the REST API of a Spring Boot application over HTTPS protocol to a generic client. HTTPS can be interpreted as HTTP plus SSL protocol. Usually, this is implemented by the gateway component of a micro-service system since all the external requests by generic clients (usually browsers) have to go through it. We want to ensure that all information returned by the server is available as plain data only to the client part, and not understandable by any program that could intercept the communication somehow.
We need a certificate to configure the server to expose its API over HTTPS. If we work in a development environment, we can generate a self-signed certificate. We can use the Java keytool application in the command prompt for this:
keytool -genkey -alias books -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore books.p12 -validity 3650 -dname "CN=codingstrain,OU=cs,O=cs,C=IT" -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -storepass mypass -keypass mypass
The above command generates a pair of asymmetric keys and stores them in a keystore named books.p12. We can copy this file in the application resources folder to make it available on the classpath. Then we have to edit the application.yaml file and add the following configuration:
server:
port: ${PORT:8092}
ssl:
enabled: true
key-store: classpath:books.p12
key-store-password: mypass
key-store-type: PKCS12
security:
require-ssl: true
With the above configuration, the server API will be exposed through HTTPS/SSL, and the data will be encrypted by the private key available in the key-store file.
Securing Services and Discovery Server with SSL protocol
Forcing a Spring Boot application to expose its API through HTTPS and the SSL protocol is a first step toward security, but it’s not enough. A micro-service system is made of services interacting with each other and usually interacting with the external world through a gateway component. We don’t want external clients to be able to directly call services behind the gateway.
We expect our services to accept calls only from other internal services, plus the gateway. In other words, we must have a set of mutually trusted services. To do this, we must configure them to implement a bi-directional SSL connection, in which each node must authenticate the other node before initiating the communication. With this in place, and the exchange data being encrypted, we have a more solid security.
Since our services normally register themselves with a discovery server and fetch other services from it, we also have to secure this mutual interaction. We will therefore describe here the configuration of a specific server and an Eureka discovery server as an example that can be generalized for any couple of services.
We can still use the keytool Java application to generate self-signed certificates. Besides storing the key-store file generated for the service in its resources folder, along with the private key, we also have to provide the public key to the Eureka server. Vice versa, we must provide the public key of the Eureka server to the service.
We provide the public key by importing it into a trust store, which will then be stored in the application that needs to authenticate the other, in the resources folder. Supposing we have a discovery application and a service named books, we can proceed to create the self-signed certificates for the two:
keytool -genkey -alias discovery -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore discovery.p12 -validity 3650 -dname "CN=codingstrain,OU=cs,O=cs,C=IT" -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -storepass mypass -keypass mypass
keytool -genkey -alias books -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore books.p12 -validity 3650 -dname "CN=codingstrain,OU=cs,O=cs,C=IT" -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -storepass mypass -keypass mypass
We obtain two key stores that contain pairs of private and public keys. We then extract the certificates containing the public keys with the following commands:
keytool -exportcert -alias discovery -keystore discovery.p12 -file discovery.cer
keytool -exportcert -alias books -keystore books.p12 -file books.cer
Finally, we import the discovery certificate into the books key-store and the books certificate into the discovery key-store. In this example, we used the same file for two purposes: holding the private key for the specific node and the public key certificate of the service it needs to interact with. We could also use separate store files; it’s just a matter of convenience. From the standpoint of storing the public key certificate, the key-store file can be called a trust-store. We can see here how we can perform the import:
keytool -importcert -alias books -keystore discovery.p12 -file books.cer
keytool -importcert -alias discovery -keystore books.p12 -file discovery.cer
Configuring the Discovery Server to use SSL Protocol
After having put the key-store/trust-store file in the resources folder of the discovery server, we can configure it by editing the application.yaml file:
server:
port: ${PORT:8760}
ssl:
enabled: true
client-auth: need
key-store: classpath:discovery.p12
key-store-type: PKCS12
key-store-password: mypass
trust-store: classpath:discovery.p12
trust-store-type: PKCS12
trust-store-password: mypass
key-alias: discovery
security:
require-ssl: true
eureka:
instance:
hostname: localhost
securePort: ${server.port}
securePortEnabled: true
nonSecurePortEnabled: false
secureVirtualHostName: ${spring.application.name}
homePageUrl: https://${eureka.instance.hostname}:${server.port}/
statusPageUrl: https://${eureka.instance.hostname}:${server.port}/admin/info
metadataMap:
hostname : ${eureka.instance.hostname}
securePort: ${server.port}
client:
registerWithEureka: false
fetchRegistry: false
Firstly, we set the security:require-ssl property to true, to force the application to serve external calls only through SSL, and thus by HTTPS protocol. Then the server:SSL property will contain the configuration for the key-store and trust-store: the type, which is PKCS12 in this case, the path of the file on the classpath, and the password.
We configure the eureka:instance section with securePortEnabled to true and nonSecurePortEnabled to false, forcing a secure communication mode with the services. Finally, in the eureka:client section we simply state that the discovery server should not register itself and should not fetch services from itself.
Configuring the Micro-Service to use SSL Protocol
The configuration for the micro-services is similar, except eureka:client part that does not contain the registerWithEureka and fetchRegistry properties, which are by default true. The eureka:client section contains the URL of the discovery server and the paths and passwords of the key-store and trust-store files.
server:
port: ${PORT:8092}
ssl:
enabled: true
key-store: classpath:books.p12
key-store-password: mypass
key-store-type: PKCS12
trust-store: classpath:books.p12
trust-store-password: mypass
security:
require-ssl: true
eureka:
instance:
hostname: localhost
securePort: ${server.port}
securePortEnabled: true
nonSecurePortEnabled: false
secureVirtualHostName: ${spring.application.name}
homePageUrl: https://${eureka.instance.hostname}:${server.port}/
statusPageUrl: https://${eureka.instance.hostname}:${server.port}/admin/info
metadataMap:
hostname : ${eureka.instance.hostname}
securePort: ${server.port}
client:
serviceUrl:
defaultZone: 'https://localhost:8760/eureka/'
tls:
enabled: true
key-store: classpath:books.p12
key-store-type: PKCS12
key-store-password: mypass
key-password: mypass
trust-store: classpath:books.p12
trust-store-type: PKCS12
trust-store-password: mypass
For the discovery client side, there is one last thing to configure. Each service interacts with Eureka through a client RestTemplate component. We have to instruct the RestTemplate client to use SSL, and we can do it by providing a custom version of the bean in Spring Boot:
@SpringBootApplication
@RefreshScope
public class AppMain {
@Value("${eureka.client.tls.trust-store}")
private String trustStore;
@Value("${eureka.client.tls.trust-store-password}")
private String trustStorePassword;
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
@Bean
public RestTemplate restTemplate() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, CertificateException,
MalformedURLException,
IOException {
URL url = new URL(trustStore);
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(url, trustStorePassword.toCharArray())
.build();
SSLConnectionSocketFactory sslConFactory = new SSLConnectionSocketFactory(sslContext);
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslConFactory)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(requestFactory);
}
}
Completing the Architecture with a Configuration Server
In the above sections, we explained how to configure the microservices and the discovery server to enforce secure communication through SSL protocol. We want to improve our architecture by providing a configuration server and storing services and the discovery server configuration on it. We also want to encrypt the passwords in the configuration files.
To implement a configuration server, we have to include the spring-cloud-config-server dependency, and the spring-boot-starter-security starter to enforce a basic user/password authentication and the encrypt/decrypt functionality.
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
We should also annotate the main Spring Boot class with @EnableConfigServer:
@SpringBootApplication
@EnableConfigServer
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
}
To enable encrypt/decrypt functionality, we should also generate a key-store with a couple of public and private keys, as we did before for the micro-service and discovery server, and store it in the resources folder. Then we have to provide the following configuration in the application.yaml file.
server:
port: ${PORT:8888}
spring:
application:
name: config-service
security:
user:
name: myusername
password: mypassword
cloud:
config:
server:
git:
uri: https://github.com/mcasari/spring-cloud-config-git-server-repo.git
cloneOnStart: true
encrypt:
keyStore:
location: classpath:/config.p12
password: mypass
alias: config
secret: mypass
To authenticate with the configuration server, we provide a simple username and password. The micro-service and discovery server configuration will be stored on git on the https://github.com/mcasari/spring-cloud-config-git-server-repo.git repository. The encrypt section defines the keystore and allows the server to expose an /encrypt endpoint that can be used to encrypt the passwords (or other sensitive information) to be stored in the configuration files.
We also have to disable the /decrypt endpoint, because this feature must be used only internally by the server to decrypt the configuration data. We do this by a SecurityFilterChain, denying access to the endpoint and permitting it for anything else:
@SpringBootApplication
@EnableConfigServer
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.requestMatchers("/decrypt").denyAll().anyRequest().permitAll())
.csrf(CsrfConfigurer::disable);
return http.build();
}
}
To encrypt a password we can do this in the command line:
curl localhost:8888/encrypt -s -d mypass
The encrypted password can then be included in the application.yaml file by prefixing it with {cipher}. You can see an example in the following fragment:
server:
port: ${PORT:8092}
ssl:
enabled: true
key-store: classpath:books.p12
key-store-password: '{cipher}AQA0oPBAK1my1NcL3WUbl5LnJzDQ1bHEbmqhLyb0SRMqr7QWrPihDwjBxVMuWd8TD3Z47Dk1lJAdrvHUKBQEk3xfrPlHa2ioxZvaVYJhpUi84csdKNjHsbGclB9DNkGu90oVhUMuRxBTzjwcqA50KGUEQuxTYZ0zOye/kfUQ6THco7c3swUq6ni7VuB+d7KajBVct+yuND6o4tJ++WiJRWBsbEUqt/emO2kxRfmeTAFL3ExLvrLFmHT56KE4bEij1sDQBQucYH/oPL1Cmu+eiOMb91Kg4IAEfOZjSnlqFLWYDTZElghSx6WImxMUSmHc6zYRgICS6oQOI3zFIWeMqYomha4IcU0qx+pLMCwrYYrD99IM1bxL7y0nC2+x1SH4qbw='
key-store-type: PKCS12
trust-store: classpath:books.p12
trust-store-password: '{cipher}AQA0oPBAK1my1NcL3WUbl5LnJzDQ1bHEbmqhLyb0SRMqr7QWrPihDwjBxVMuWd8TD3Z47Dk1lJAdrvHUKBQEk3xfrPlHa2ioxZvaVYJhpUi84csdKNjHsbGclB9DNkGu90oVhUMuRxBTzjwcqA50KGUEQuxTYZ0zOye/kfUQ6THco7c3swUq6ni7VuB+d7KajBVct+yuND6o4tJ++WiJRWBsbEUqt/emO2kxRfmeTAFL3ExLvrLFmHT56KE4bEij1sDQBQucYH/oPL1Cmu+eiOMb91Kg4IAEfOZjSnlqFLWYDTZElghSx6WImxMUSmHc6zYRgICS6oQOI3zFIWeMqYomha4IcU0qx+pLMCwrYYrD99IM1bxL7y0nC2+x1SH4qbw='
Conclusion
The first step in securing a micro-service system is to force the communication and mutual authentication of services through SSL protocol. It is also important to encrypt sensitive information in the configuration files hosted on the servers. In this article, we have seen a typical architecture with the configuration centralized in a configuration server, and the services interacting in protected mode through a discovery service.
You can find the source code of the example in this article on GitHub.
