Skip to content

Commit a97c6f7

Browse files
committed
Added Spring Boot Starter gRPC SSL reloading
1 parent e97657a commit a97c6f7

File tree

11 files changed

+374
-1
lines changed

11 files changed

+374
-1
lines changed

README.MD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ A repository containing different java tutorials
2727
- Instant Server SSL Reloading
2828
- [Spring Boot and Jetty](instant-server-ssl-reloading)
2929
- [Spring Boot and Tomcat](instant-ssl-reloading-with-spring-tomcat)
30+
- [Spring Boot gRPC](instant-ssl-reloading-with-spring-grpc)
3031
- [Vert.x](instant-server-ssl-reloading-with-vertx/vertx-server)
3132
- [Netty](instant-server-ssl-reloading-with-netty/netty-server)
3233
- [gRPC](grpc-client-server-with-ssl/instant-server-ssl-reloading-with-grpc)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Instant SSL Reloading 🔐
2+
A server configured with ssl has a key material and trust material. These materials are generated from a keypair and certificate which always have an expiration date.
3+
In a traditional server configuration a reboot of the server is required to apply the latest key material and trust material changes when the keystore and truststore are changed.
4+
A downtime is therefore unavoidable. This project demonstrates with a basic setup how update the server certificate from an external source without the need of restarting your server. In this way you can achieve zero downtime.
5+
6+
The repository contains:
7+
- Server, based on Spring Boot with Netty as a server engine for gRPC
8+
9+
### SSL Updating entrypoint for the server:
10+
The server has two ways to update the existing ssl material:
11+
- File based aka file change listener, see here for the implementation: [FilesBasedSslUpdateService](src/main/java/nl/altindag/server/service/FileBasedSslUpdateService.java)
12+
- Databased based, aka database change listener. This option is hosted in a separate module within this repository, see here: [Instant SSL Reloading With Database](https://github.com/Hakky54/java-tutorials/tree/main/instant-ssl-reloading-with-spring-jetty-database)
13+
14+
#### Requirements
15+
- Java 21
16+
- Terminal
17+
18+
#### Start the server
19+
```
20+
mvn spring-boot:run
21+
```
22+
Visit the server with the following url on your browser: https://localhost:8443
23+
Open the certificate details in your browser by clicking on the lock logo (on Chrome). You will see a similar certificate detail as shown below:
24+
25+
![alt text](https://github.com/Hakky54/java-tutorials/blob/main/instant-server-ssl-reloading/images/before-reloading.png?raw=true)
26+
27+
Please note down the expiration date. Afterwords you will compare it when you have run the admin application.
28+
29+
#### Refresh the server certificates with the file listener
30+
The file based ssl update service will listen to changes on a specific file on the file system. Adjust the path to your identity and truststore within [FilesBasedSslUpdateService](server/src/main/java/nl/altindag/server/service/FileBasedSslUpdateService.java).
31+
```java
32+
private static final Path identityPath = Path.of("/path/to/your/identity.jks");
33+
private static final Path trustStorePath = Path.of("/path/to/your/truststore.jks");
34+
```
35+
Also change the passwords if it is different.
36+
```java
37+
private static final char[] identityPassword = "secret".toCharArray();
38+
private static final char[] trustStorePassword = "secret".toCharArray();
39+
```
40+
Adjust the content of the identity and truststore and after 10 seonds the cron job will be triggered to validate if the content has been changed, and it will update the ssl configuration if there are any changes on it.
41+
42+
Refresh your browser tab and open the certificate details again and compare the expiration date with the one you have noted down.
43+
You should have a similar certificate detail as shown below:
44+
45+
![alt text](https://github.com/Hakky54/java-tutorials/blob/main/instant-server-ssl-reloading/images/after-reloading.png?raw=true)
46+
47+
#### Testing with a client
48+
49+
Run the App from the client module [here](https://github.com/Hakky54/java-tutorials/blob/main/grpc-client-server-with-ssl/grpc-client/src/main/java/nl/altindag/grpc/client/App.java)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>io.github.hakky54</groupId>
9+
<artifactId>java-tutorials</artifactId>
10+
<version>1.0.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>instant-ssl-reloading-with-spring-grpc</artifactId>
14+
<version>1.0.0-SNAPSHOT</version>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>io.github.hakky54</groupId>
19+
<artifactId>sslcontext-kickstart-for-netty</artifactId>
20+
<version>${version.sslcontext-kickstart}</version>
21+
</dependency>
22+
<dependency>
23+
<groupId>io.grpc</groupId>
24+
<artifactId>grpc-services</artifactId>
25+
<version>${version.grpc}</version>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.springframework.grpc</groupId>
29+
<artifactId>spring-grpc-spring-boot-starter</artifactId>
30+
<version>${version.spring-grpc-spring-boot-starter}</version>
31+
</dependency>
32+
<dependency>
33+
<groupId>io.github.hakky54</groupId>
34+
<artifactId>common-proto</artifactId>
35+
<version>1.0.0-SNAPSHOT</version>
36+
</dependency>
37+
</dependencies>
38+
39+
</project>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2022 Thunderberry.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nl.altindag.server;
17+
18+
import org.springframework.boot.SpringApplication;
19+
import org.springframework.boot.autoconfigure.SpringBootApplication;
20+
import org.springframework.scheduling.annotation.EnableScheduling;
21+
22+
@EnableScheduling
23+
@SpringBootApplication
24+
public class App {
25+
26+
public static void main(String[] args) {
27+
SpringApplication.run(App.class, args);
28+
}
29+
30+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2022 Thunderberry.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nl.altindag.server.config;
17+
18+
import nl.altindag.ssl.SSLFactory;
19+
import org.springframework.beans.factory.annotation.Value;
20+
import org.springframework.boot.ssl.NoSuchSslBundleException;
21+
import org.springframework.boot.ssl.SslBundle;
22+
import org.springframework.boot.ssl.SslBundleKey;
23+
import org.springframework.boot.ssl.SslBundles;
24+
import org.springframework.boot.ssl.SslManagerBundle;
25+
import org.springframework.boot.ssl.SslOptions;
26+
import org.springframework.boot.ssl.SslStoreBundle;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
30+
import javax.net.ssl.KeyManagerFactory;
31+
import javax.net.ssl.TrustManagerFactory;
32+
import java.util.Collections;
33+
import java.util.List;
34+
import java.util.function.BiConsumer;
35+
import java.util.function.Consumer;
36+
37+
@Configuration
38+
public class SSLConfig {
39+
40+
@Bean
41+
public SSLFactory sslFactory(@Value("${ssl.keystore-path}") String keyStorePath,
42+
@Value("${ssl.keystore-password}") char[] keyStorePassword,
43+
@Value("${ssl.truststore-path}") String trustStorePath,
44+
@Value("${ssl.truststore-password}") char[] trustStorePassword,
45+
@Value("${ssl.client-auth}") boolean isClientAuthenticationRequired) {
46+
47+
return SSLFactory.builder()
48+
.withSwappableIdentityMaterial()
49+
.withSwappableTrustMaterial()
50+
.withIdentityMaterial(keyStorePath, keyStorePassword)
51+
.withTrustMaterial(trustStorePath, trustStorePassword)
52+
.withNeedClientAuthentication(isClientAuthenticationRequired)
53+
.build();
54+
}
55+
56+
@Bean
57+
public SslBundles sslBundles(SSLFactory sslFactory) {
58+
return new SslBundles() {
59+
@Override
60+
public SslBundle getBundle(String name) throws NoSuchSslBundleException {
61+
return new SslBundle() {
62+
@Override
63+
public SslStoreBundle getStores() {
64+
return null;
65+
}
66+
67+
@Override
68+
public SslBundleKey getKey() {
69+
return null;
70+
}
71+
72+
@Override
73+
public SslOptions getOptions() {
74+
return null;
75+
}
76+
77+
@Override
78+
public String getProtocol() {
79+
return "";
80+
}
81+
82+
@Override
83+
public SslManagerBundle getManagers() {
84+
return new SslManagerBundle() {
85+
@Override
86+
public KeyManagerFactory getKeyManagerFactory() {
87+
return sslFactory.getKeyManagerFactory().get();
88+
}
89+
90+
@Override
91+
public TrustManagerFactory getTrustManagerFactory() {
92+
return sslFactory.getTrustManagerFactory().get();
93+
}
94+
};
95+
}
96+
};
97+
}
98+
99+
@Override
100+
public void addBundleUpdateHandler(String name, Consumer<SslBundle> updateHandler) throws NoSuchSslBundleException {
101+
102+
}
103+
104+
@Override
105+
public void addBundleRegisterHandler(BiConsumer<String, SslBundle> registerHandler) {
106+
107+
}
108+
109+
@Override
110+
public List<String> getBundleNames() {
111+
return Collections.emptyList();
112+
}
113+
};
114+
}
115+
116+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2022 Thunderberry.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nl.altindag.server.service;
17+
18+
import nl.altindag.ssl.SSLFactory;
19+
import nl.altindag.ssl.util.SSLFactoryUtils;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.scheduling.annotation.Scheduled;
23+
import org.springframework.stereotype.Service;
24+
25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.attribute.BasicFileAttributes;
29+
import java.time.Instant;
30+
import java.time.ZoneOffset;
31+
import java.time.ZonedDateTime;
32+
33+
@Service
34+
public class FileBasedSslUpdateService {
35+
36+
private static final Logger LOGGER = LoggerFactory.getLogger(FileBasedSslUpdateService.class);
37+
38+
private static final Path identityPath = Path.of("/path/to/your/identity.jks");
39+
private static final Path trustStorePath = Path.of("/path/to/your/truststore.jks");
40+
private static final char[] identityPassword = "secret".toCharArray();
41+
private static final char[] trustStorePassword = "secret".toCharArray();
42+
43+
private ZonedDateTime lastModifiedTimeIdentityStore = ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
44+
private ZonedDateTime lastModifiedTimeTrustStore = ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
45+
46+
private final SSLFactory baseSslFactory;
47+
48+
public FileBasedSslUpdateService(SSLFactory baseSslFactory) {
49+
this.baseSslFactory = baseSslFactory;
50+
}
51+
52+
/**
53+
* Checks every 10 seconds if the keystore files have been updated.
54+
* If the files have been updated the service will read the content and update the ssl material
55+
* within the existing ssl configuration.
56+
*/
57+
@Scheduled(cron = "*/10 * * * * *")
58+
private void updateSslMaterial() throws IOException {
59+
if (Files.exists(identityPath) && Files.exists(trustStorePath)) {
60+
BasicFileAttributes identityAttributes = Files.readAttributes(identityPath, BasicFileAttributes.class);
61+
BasicFileAttributes trustStoreAttributes = Files.readAttributes(trustStorePath, BasicFileAttributes.class);
62+
63+
boolean identityUpdated = lastModifiedTimeIdentityStore.isBefore(ZonedDateTime.ofInstant(identityAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC));
64+
boolean trustStoreUpdated = lastModifiedTimeTrustStore.isBefore(ZonedDateTime.ofInstant(trustStoreAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC));
65+
66+
if (identityUpdated && trustStoreUpdated) {
67+
LOGGER.info("Keystore files have been changed. Trying to read the file content and preparing to update the ssl material");
68+
69+
SSLFactory updatedSslFactory = SSLFactory.builder()
70+
.withIdentityMaterial(identityPath, identityPassword)
71+
.withTrustMaterial(trustStorePath, trustStorePassword)
72+
.build();
73+
74+
SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
75+
76+
lastModifiedTimeIdentityStore = ZonedDateTime.ofInstant(identityAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC);
77+
lastModifiedTimeTrustStore = ZonedDateTime.ofInstant(trustStoreAttributes.lastModifiedTime().toInstant(), ZoneOffset.UTC);
78+
79+
LOGGER.info("Updating ssl material finished");
80+
}
81+
}
82+
}
83+
84+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2022 Thunderberry.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nl.altindag.server.service;
17+
18+
import io.grpc.stub.StreamObserver;
19+
import nl.altindag.grpc.HelloRequest;
20+
import nl.altindag.grpc.HelloResponse;
21+
import nl.altindag.grpc.HelloServiceGrpc;
22+
import org.springframework.stereotype.Service;
23+
24+
@Service
25+
public class GrpcServerService extends HelloServiceGrpc.HelloServiceImplBase {
26+
27+
@Override
28+
public void hello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
29+
HelloResponse response = HelloResponse.newBuilder()
30+
.setMessage(String.format("Hello %s, nice to meet you!", request.getName()))
31+
.build();
32+
33+
responseObserver.onNext(response);
34+
responseObserver.onCompleted();
35+
}
36+
37+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ssl:
2+
client-auth: true
3+
keystore-path: identity.jks
4+
keystore-password: secret
5+
truststore-path: truststore.jks
6+
truststore-password: secret
7+
8+
spring:
9+
grpc:
10+
server:
11+
port: 8443
12+
ssl:
13+
enabled: true
14+
client-auth: require
15+
secure: true
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)