├── .github └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── client-bob ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── jonashackt │ │ │ ├── BobApplication.java │ │ │ ├── client │ │ │ ├── ServerClient.java │ │ │ └── ServerClientImpl.java │ │ │ ├── configuration │ │ │ ├── OpenAPIConfig.java │ │ │ └── RestClientCertConfiguration.java │ │ │ └── controller │ │ │ └── BobController.java │ └── resources │ │ ├── alice-keystore.p12 │ │ ├── application.yml │ │ ├── client-truststore.jks │ │ └── tom-keystore.p12 │ └── test │ └── java │ └── de │ └── jonashackt │ └── BobTest.java ├── client-truststore.png ├── docker-compose.yml ├── docker-network-client ├── pom.xml └── src │ └── test │ └── java │ └── de │ └── jonashackt │ └── ClientTest.java ├── pom.xml ├── renovate.json ├── server-alice ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── jonashackt │ │ │ ├── ServerAliceApplication.java │ │ │ ├── configuration │ │ │ └── WebSecurityConfig.java │ │ │ └── controller │ │ │ └── ServerAliceController.java │ └── resources │ │ ├── alice-csr.conf │ │ ├── alice-keystore.jks │ │ ├── alice-keystore.p12 │ │ ├── alice-truststore.jks │ │ ├── alice.crt │ │ ├── alice.csr │ │ ├── aliceprivate.key │ │ └── application.yml │ └── test │ ├── java │ └── de │ │ └── jonashackt │ │ ├── RestClientCertTest.java │ │ └── RestClientCertTestConfiguration.java │ └── resources │ ├── alice-keystore.p12 │ └── alice-truststore.jks └── server-tom ├── Dockerfile ├── pom.xml └── src ├── main ├── java │ └── de │ │ └── jonashackt │ │ ├── ServerTomApplication.java │ │ ├── configuration │ │ └── WebSecurityConfig.java │ │ └── controller │ │ └── ServerTomController.java └── resources │ ├── application.yml │ ├── tom-csr.conf │ ├── tom-keystore.p12 │ ├── tom-truststore.jks │ ├── tom.crt │ ├── tom.csr │ └── tomprivate.key └── test ├── java └── de │ └── jonashackt │ ├── RestClientCertTest.java │ └── RestClientCertTestConfiguration.java └── resources ├── tom-keystore.p12 └── tom-truststore.jks /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: github 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up JDK 15 13 | uses: actions/setup-java@v2 14 | with: 15 | distribution: 'adopt' 16 | java-version: 15 17 | 18 | - name: Build with Maven 19 | run: mvn -B install --no-transfer-progress --file pom.xml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.springBeans 3 | 4 | # Package Files # 5 | *.jar 6 | *.war 7 | *.ear 8 | 9 | # Eclipse # 10 | .settings 11 | .project 12 | .classpath 13 | .studio 14 | target 15 | 16 | # Apple # 17 | .DS_Store 18 | 19 | # Intellij # 20 | .idea 21 | *.iml 22 | *.log 23 | 24 | # logback 25 | logback.out.xml 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jonas Hecht 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Multiple Spring Boot servers that are secured with different client certificates - called by RestTemplate 2 | ============================= 3 | [![Build Status](https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/workflows/github/badge.svg)](https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/actions) 4 | [![renovateenabled](https://img.shields.io/badge/renovate-enabled-yellow)](https://renovatebot.com) 5 | 6 | This repository basically forks all the ground work that was done in https://github.com/jonashackt/spring-boot-rest-clientcertificate. This is a basic example, where the client certificate secured server is a Spring Boot Application and the client is just a Testcase that uses Spring´s RestTemplate which is configured to use the client certificate. 7 | 8 | In contrast the present project focusses on the configuration of more than one client certificates and how to access REST endpoints from multiple servers that are secured by different client certificates with Spring´s RestTemplate. 9 | 10 | Therefore we use several Spring Boot based microservices that provide different client certificate secured REST endpoint and a separate microservice that accesses these services: 11 | 12 | ``` 13 | ------------------------------------------- 14 | | Docker Network scope __ | 15 | | ============|o¬| | 16 | | = ¯¯= | 17 | | = server-alice = | 18 | ============ | ============== ssl = = | 19 | = docker- = | = = -----> ================ | 20 | = network- = -----> = client-bob = __ | 21 | = client = | = __ __ = -----> ============|o¬| | 22 | ============ | ===|o¬|=|o¬|== ssl = ¯¯= | 23 | | ¯¯ ¯¯ = server-tom = | 24 | | = = | 25 | | ================ | 26 | ------------------------------------------- 27 | 28 | ``` 29 | 30 | 31 | For a general approach on how to generate private keys and certificates and create Java Keystores, have a look into https://github.com/jonashackt/spring-boot-rest-clientcertificate#generate-the-usual-key-and-crt---and-import-them-into-needed-keystore-jks-files 32 | 33 | # HowTo Use 34 | 35 | Everything you need to run a full build and __complete__ test (incl. Integrationtest of docker-network-client firing up all three microservices that´ll call each other with client certificate support) is this: 36 | 37 | ``` 38 | mvn clean install 39 | ``` 40 | 41 | Only, if you want to check manually, you can do a `docker-compose up -d` and open your Browser with [http:localhost:8080/swagger-ui.html] and fire up a GET-Request to /secretservers with Swagger :) 42 | 43 | 44 | # Integrationtesting with [testcontainers](https://www.testcontainers.org) 45 | 46 | As client-bob only has access to the DNS aliases `server-alice` and `server-tom`, if it itself is part of the Docker (Compose) network and these aliases are used to access both client certificate secured endpoints, we need another way to run an Integration test inside the Docker network scope. 47 | 48 | Therefore we use the [testcontainers](https://www.testcontainers.org) and the __docker-network-client__ that just calls __client-bob__ inside the Docker network. 49 | 50 | testcontainers could be []simply integrated by via Maven](https://www.testcontainers.org/usage.html#maven-dependencies): 51 | 52 | ``` 53 | 54 | org.testcontainers 55 | testcontainers 56 | NewestTestcontainersVersionOnMavenCentral 57 | test 58 | 59 | ``` 60 | 61 | And the code you need, to fire up all Docker Compose services / Docker Containers is really simple: 62 | 63 | ``` 64 | package de.jonashackt; 65 | 66 | import org.apache.http.HttpStatus; 67 | import org.junit.ClassRule; 68 | import org.junit.Test; 69 | import org.junit.runner.RunWith; 70 | import org.springframework.test.context.ContextConfiguration; 71 | import org.springframework.test.context.junit4.SpringRunner; 72 | import org.testcontainers.containers.DockerComposeContainer; 73 | import org.testcontainers.containers.wait.strategy.Wait; 74 | 75 | import java.io.File; 76 | 77 | import static io.restassured.RestAssured.when; 78 | import static org.hamcrest.Matchers.containsString; 79 | 80 | @RunWith(SpringRunner.class) 81 | @ContextConfiguration() 82 | public class ClientTest { 83 | 84 | @ClassRule 85 | public static DockerComposeContainer services = 86 | new DockerComposeContainer(new File("../docker-compose.yml")) 87 | .withExposedService("server-alice", 8443,Wait.forListeningPort()) 88 | .withExposedService("server-tom", 8443, Wait.forListeningPort()) 89 | .withExposedService("client-bob", 8080, Wait.forHttp("/swagger-ui.html").forStatusCode(200)).withLocalCompose(true); 90 | 91 | 92 | @Test 93 | public void is_client_bob_able_to_call_all_servers_with_client_certs() { 94 | 95 | when() 96 | .get("http://localhost:8080/secretservers") 97 | .then() 98 | .statusCode(HttpStatus.SC_OK) 99 | .assertThat() 100 | .body(containsString("Both Servers called - Alice said 'Alice answering!' & Tom replied 'Tom answering!'.")); 101 | } 102 | } 103 | 104 | ``` 105 | 106 | # TlDR: How to create multiple keys & certificates for multiple servers - and add these into appropriate truststores / keystores 107 | 108 | 109 | ## server-alice keys and client certificate, truststore & keystore (see /server-alice/src/main/resources) 110 | 111 | #### 1. Private Key: aliceprivate.key 112 | 113 | ``` 114 | openssl genrsa -des3 -out aliceprivate.key 1024 115 | ``` 116 | 117 | - passphrase `alicepassword` 118 | 119 | 120 | #### 2. Certificate Signing Request (CSR): alice.csr 121 | 122 | ``` 123 | openssl req -new -key aliceprivate.key -out alice.csr -config alice-csr.conf 124 | ``` 125 | 126 | __Common Name__: `server-alice`, which will later be a DNS alias inside the Docker network 127 | 128 | 129 | #### 3. self-signed Certificate: alice.crt 130 | 131 | ``` 132 | openssl x509 -req -days 3650 -in alice.csr -signkey aliceprivate.key -out alice.crt -extfile alice-csr.conf -extensions v3_req 133 | ``` 134 | 135 | 136 | #### 4. Java Truststore Keystore, that inherits the generated self-signed Certificate: alice-truststore.jks 137 | 138 | ``` 139 | keytool -import -file alice.crt -alias alicesCA -keystore alice-truststore.jks 140 | ``` 141 | 142 | __the same password__ `alicepassword` 143 | 144 | 145 | #### 5. Java Keystore, that inherits Public and Private Keys (keypair): alice-keystore.jks 146 | 147 | ``` 148 | openssl pkcs12 -export -in alice.crt -inkey aliceprivate.key -certfile alice.crt -name "alicecert" -out alice-keystore.p12 149 | ``` 150 | 151 | __the same password__ `alicepassword` 152 | 153 | To read in KeyStore Explorer 154 | 155 | ``` 156 | keytool -importkeystore -srckeystore alice-keystore.p12 -srcstoretype pkcs12 -destkeystore alice-keystore.jks -deststoretype JKS 157 | ``` 158 | 159 | 160 | 161 | ## server-tom keys and client certificate, truststore & keystore (see /server-tom/src/main/resources) 162 | 163 | #### 1. Private Key: tomprivate.key 164 | 165 | ``` 166 | openssl genrsa -des3 -out tomprivate.key 1024 167 | ``` 168 | 169 | - passphrase `tompassword` 170 | 171 | 172 | #### 2. Certificate Signing Request (CSR): tom.csr 173 | 174 | ``` 175 | openssl req -new -key tomprivate.key -out tom.csr -config tom-csr.conf 176 | ``` 177 | 178 | __Common Name__: `server-tom`, which will later be a DNS alias inside the Docker network 179 | 180 | 181 | #### 3. self-signed Certificate: tom.crt 182 | 183 | ``` 184 | openssl x509 -req -days 3650 -in tom.csr -signkey tomprivate.key -out tom.crt -extfile tom-csr.conf -extensions v3_req 185 | ``` 186 | 187 | 188 | #### 4. Java Truststore Keystore, that inherits the generated self-signed Certificate: tom-truststore.jks 189 | 190 | ``` 191 | keytool -import -file tom.crt -alias tomsCA -keystore tom-truststore.jks 192 | ``` 193 | 194 | __the same password__ `tompassword` 195 | 196 | 197 | #### 5. Java Keystore, that inherits Public and Private Keys (keypair): tom-keystore.p12 198 | 199 | ``` 200 | openssl pkcs12 -export -in tom.crt -inkey tomprivate.key -certfile tom.crt -name "tomcert" -out tom-keystore.p12 201 | ``` 202 | 203 | __the same password__ `tompassword` 204 | 205 | 206 | 207 | ## client-bob truststore & keystore (see /server-alice/src/main/resources) 208 | 209 | #### 1. Java Truststore Keystore, that inherits the generated self-signed Certificate: client-truststore.jks 210 | 211 | ``` 212 | keytool -import -file alice.crt -alias alicesCA -keystore client-truststore.jks 213 | keytool -import -file tom.crt -alias tomsCA -keystore client-truststore.jks 214 | ``` 215 | 216 | __password__ `bobpassword` 217 | 218 | In KeyStore Explorer this should look like this: 219 | 220 | ![client-truststore](https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/blob/master/client-truststore.png) 221 | 222 | 223 | #### 2. Java Keystores, that inherit Public and Private Keys (keypair): copy alice-keystore.p12 & tom-keystore.p12 224 | 225 | As Apache HttpClient isn´t able to handle [more than one client certificate for the same SSLContext](http://mail-archives.apache.org/mod_mbox/hc-httpclient-users/201109.mbox/%3C1315998630.3176.17.camel@ubuntu%3E), we need to provide two of them. Therefore we don´t need to add two private keys and certificates to one Keystore - we can just use both Keystores we already assembled before. So we copy `alice-keystore.p12` & `tom-keystore.p12` to clien-bob/src/main/resources and use them in the [RestClientCertConfiguration](https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/blob/master/client-bob/src/main/java/de/jonashackt/configuration/RestClientCertConfiguration.java) like this: 226 | 227 | ``` 228 | import org.apache.commons.io.FileUtils; 229 | import org.apache.http.client.HttpClient; 230 | import org.apache.http.impl.client.HttpClients; 231 | import org.apache.http.ssl.SSLContextBuilder; 232 | import org.springframework.beans.factory.annotation.Value; 233 | import org.springframework.context.annotation.Bean; 234 | import org.springframework.context.annotation.Configuration; 235 | import org.springframework.core.io.Resource; 236 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 237 | 238 | import javax.net.ssl.SSLContext; 239 | import java.io.File; 240 | import java.io.IOException; 241 | 242 | @Configuration 243 | public class RestClientCertConfiguration { 244 | 245 | private char[] bobPassword = "bobpassword".toCharArray(); 246 | private char[] tomPassword = "tompassword".toCharArray(); 247 | 248 | @Value("classpath:alice-keystore.p12") 249 | private Resource aliceKeystoreResource; 250 | 251 | @Value("classpath:tom-keystore.p12") 252 | private Resource tomKeystoreResource; 253 | 254 | @Value("classpath:client-truststore.jks") 255 | private Resource truststoreResource; 256 | private char[] alicePassword = "alicepassword".toCharArray(); 257 | 258 | @Bean 259 | public HttpComponentsClientHttpRequestFactory serverTomClientHttpRequestFactory() throws Exception { 260 | SSLContext sslContext = SSLContextBuilder 261 | .create() 262 | .loadKeyMaterial(inStream2File(tomKeystoreResource), tomPassword, tomPassword) 263 | .loadTrustMaterial(inStream2File(truststoreResource), bobPassword) 264 | .build(); 265 | 266 | HttpClient client = HttpClients.custom() 267 | .setSSLContext(sslContext) 268 | .build(); 269 | 270 | return new HttpComponentsClientHttpRequestFactory(client); 271 | } 272 | 273 | @Bean 274 | public HttpComponentsClientHttpRequestFactory serverAliceClientHttpRequestFactory() throws Exception { 275 | SSLContext sslContext = SSLContextBuilder 276 | .create() 277 | .loadKeyMaterial(inStream2File(aliceKeystoreResource), alicePassword, alicePassword) 278 | .loadTrustMaterial(inStream2File(truststoreResource), bobPassword) 279 | .build(); 280 | 281 | HttpClient client = HttpClients.custom() 282 | .setSSLContext(sslContext) 283 | .build(); 284 | 285 | return new HttpComponentsClientHttpRequestFactory(client); 286 | } 287 | 288 | private File inStream2File(Resource resource) { 289 | try { 290 | File tempFile = File.createTempFile("file", ".tmp"); 291 | FileUtils.copyInputStreamToFile(resource.getInputStream(), tempFile); 292 | return tempFile; 293 | } catch (IOException e) { 294 | throw new RuntimeException("Problems loading Keystores", e); 295 | } 296 | } 297 | } 298 | ``` 299 | 300 | Now we´re able to insert individual SSLContexts into Spring´s RestTemplate. Therefore see [ServerClientImpl](https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/blob/master/client-bob/src/main/java/de/jonashackt/client/ServerClientImpl.java): 301 | 302 | ``` 303 | import org.springframework.beans.factory.annotation.Autowired; 304 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 305 | import org.springframework.stereotype.Component; 306 | import org.springframework.web.client.RestTemplate; 307 | 308 | @Component 309 | public class ServerClientImpl implements ServerClient { 310 | 311 | @Autowired 312 | private HttpComponentsClientHttpRequestFactory serverAliceClientHttpRequestFactory; 313 | 314 | @Autowired 315 | private HttpComponentsClientHttpRequestFactory serverTomClientHttpRequestFactory; 316 | 317 | private RestTemplate restTemplate = new RestTemplate(); 318 | 319 | @Override 320 | public String callServerAlice() { 321 | restTemplate.setRequestFactory(serverAliceClientHttpRequestFactory); 322 | 323 | return restTemplate.getForObject("https://server-alice:8443/hello", String.class); 324 | } 325 | 326 | @Override 327 | public String callServerTom() { 328 | restTemplate.setRequestFactory(serverTomClientHttpRequestFactory); 329 | 330 | return restTemplate.getForObject("https://server-tom:8443/hello", String.class); 331 | } 332 | } 333 | ``` 334 | 335 | 336 | 337 | # Links 338 | 339 | https://stackoverflow.com/questions/25869428/classpath-resource-not-found-when-running-as-jar 340 | 341 | https://www.thomas-krenn.com/de/wiki/Openssl_Multi-Domain_CSR_erstellen 342 | 343 | https://stackoverflow.com/questions/30977264/subject-alternative-name-not-present-in-certificate 344 | 345 | https://stackoverflow.com/questions/21488845/how-can-i-generate-a-self-signed-certificate-with-subjectaltname-using-openssl 346 | 347 | --> this is not the only solution, see `-extfile` and `-extensions` CLI paramters! 348 | 349 | https://serverfault.com/questions/779475/openssl-add-subject-alternate-name-san-when-signing-with-ca 350 | 351 | #### Multiple certificates handling in Java Keystores: 352 | 353 | Look into the documentation of Tomcat in section `keyAlias`: http://tomcat.apache.org/tomcat-6.0-doc/config/http.html#SSL_Support 354 | 355 | https://stackoverflow.com/questions/5292074/how-to-specify-outbound-certificate-alias-for-https-calls 356 | 357 | https://stackoverflow.com/questions/6370745/can-we-load-multiple-certificates-keys-in-a-key-store 358 | -------------------------------------------------------------------------------- /client-bob/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | MAINTAINER Jonas Hecht 4 | 5 | VOLUME /tmp 6 | 7 | # Add Spring Boot app.jar to Container 8 | ADD "target/client-bob-0.0.1-SNAPSHOT.jar" app.jar 9 | 10 | ENV JAVA_OPTS="" 11 | 12 | # Fire up our Spring Boot app by default 13 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] -------------------------------------------------------------------------------- /client-bob/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | client-bob 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | 11 | de.jonashackt 12 | spring-boot-rest-clientcertificates-docker-compose 13 | 0.0.1-SNAPSHOT 14 | 15 | 16 | 17 | UTF-8 18 | 1.8 19 | 1.8.0 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | 30 | org.apache.httpcomponents 31 | httpclient 32 | 33 | 34 | 35 | org.springdoc 36 | springdoc-openapi-ui 37 | ${springdoc-openapi-ui.version} 38 | 39 | 40 | 41 | 42 | commons-io 43 | commons-io 44 | 2.19.0 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /client-bob/src/main/java/de/jonashackt/BobApplication.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class BobApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(BobApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client-bob/src/main/java/de/jonashackt/client/ServerClient.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.client; 2 | 3 | public interface ServerClient { 4 | String callServerAlice(); 5 | 6 | String callServerTom(); 7 | } 8 | -------------------------------------------------------------------------------- /client-bob/src/main/java/de/jonashackt/client/ServerClientImpl.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.client; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.impl.client.HttpClients; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.client.RestTemplate; 9 | 10 | import javax.net.ssl.SSLContext; 11 | 12 | @Component 13 | public class ServerClientImpl implements ServerClient { 14 | 15 | @Autowired 16 | private SSLContext serverTomSSLContext; 17 | 18 | @Autowired 19 | private SSLContext serverAliceSSLContext; 20 | 21 | private RestTemplate restTemplate = new RestTemplate(); 22 | 23 | @Override 24 | public String callServerAlice() { 25 | restTemplate.setRequestFactory(createHttpComponentsClientHttpRequestFactory(serverAliceSSLContext)); 26 | 27 | return restTemplate.getForObject("https://server-alice:8443/hello", String.class); 28 | } 29 | 30 | @Override 31 | public String callServerTom() { 32 | restTemplate.setRequestFactory(createHttpComponentsClientHttpRequestFactory(serverTomSSLContext)); 33 | 34 | return restTemplate.getForObject("https://server-tom:8443/hello", String.class); 35 | } 36 | 37 | private HttpComponentsClientHttpRequestFactory createHttpComponentsClientHttpRequestFactory(SSLContext sslContext) { 38 | HttpClient client = HttpClients.custom() 39 | .setSSLContext(sslContext) 40 | .build(); 41 | 42 | return new HttpComponentsClientHttpRequestFactory(client); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client-bob/src/main/java/de/jonashackt/configuration/OpenAPIConfig.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.configuration; 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 4 | import io.swagger.v3.oas.annotations.info.Info; 5 | import io.swagger.v3.oas.annotations.info.License; 6 | import io.swagger.v3.oas.annotations.servers.Server; 7 | 8 | @OpenAPIDefinition( 9 | info = @Info( 10 | title = "REST Client uses clientcertificate to authenticate to Spring Boot Server", 11 | version = "2.0", 12 | description = "See https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose for more information", 13 | license = @License( 14 | name = "Apache License Version 2.0", 15 | url = "https://github.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/blob/master/LICENSE" 16 | ) 17 | ), 18 | servers = @Server(url = "http://client-bob:8080") 19 | ) 20 | public class OpenAPIConfig { 21 | } -------------------------------------------------------------------------------- /client-bob/src/main/java/de/jonashackt/configuration/RestClientCertConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.configuration; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.apache.http.ssl.SSLContextBuilder; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.io.Resource; 9 | 10 | import javax.net.ssl.SSLContext; 11 | import java.io.File; 12 | import java.io.IOException; 13 | 14 | @Configuration 15 | public class RestClientCertConfiguration { 16 | 17 | private char[] bobPassword = "bobpassword".toCharArray(); 18 | private char[] tomPassword = "tompassword".toCharArray(); 19 | private char[] alicePassword = "alicepassword".toCharArray(); 20 | 21 | @Value("classpath:alice-keystore.p12") 22 | private Resource aliceKeystoreResource; 23 | 24 | @Value("classpath:tom-keystore.p12") 25 | private Resource tomKeystoreResource; 26 | 27 | @Value("classpath:client-truststore.jks") 28 | private Resource truststoreResource; 29 | 30 | @Bean 31 | public SSLContext serverTomSSLContext() throws Exception { 32 | return SSLContextBuilder 33 | .create() 34 | .loadKeyMaterial(inStream2File(tomKeystoreResource), tomPassword, tomPassword) 35 | .loadTrustMaterial(inStream2File(truststoreResource), bobPassword) 36 | .build(); 37 | } 38 | 39 | @Bean 40 | public SSLContext serverAliceSSLContext() throws Exception { 41 | return SSLContextBuilder 42 | .create() 43 | .loadKeyMaterial(inStream2File(aliceKeystoreResource), alicePassword, alicePassword) 44 | .loadTrustMaterial(inStream2File(truststoreResource), bobPassword) 45 | .build(); 46 | } 47 | 48 | 49 | /** 50 | * The ugly need to generate a File from a InputStream - because SSLContextBuild.loadKeyMaterial only accepts File, 51 | * but retrieving Files from within Spring Boot Fatjars is only possible through Resources -> 52 | * see https://stackoverflow.com/questions/25869428/classpath-resource-not-found-when-running-as-jar 53 | */ 54 | private File inStream2File(Resource resource) { 55 | try { 56 | File tempFile = File.createTempFile("file", ".tmp"); 57 | tempFile.deleteOnExit(); 58 | FileUtils.copyInputStreamToFile(resource.getInputStream(), tempFile); 59 | return tempFile; 60 | } catch (IOException e) { 61 | throw new RuntimeException("Problems loading Keystores", e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client-bob/src/main/java/de/jonashackt/controller/BobController.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.controller; 2 | 3 | import de.jonashackt.client.ServerClient; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | public class BobController { 11 | 12 | @Autowired 13 | private ServerClient serverClient; 14 | 15 | @RequestMapping(path="/secretservers", method=RequestMethod.GET) 16 | public String servercalls() { 17 | System.out.println("Calling Alice secretely!"); 18 | String serverAliceResponse = serverClient.callServerAlice(); 19 | 20 | System.out.println("Calling Tom secretely!"); 21 | String serverTomResponse = serverClient.callServerTom(); 22 | 23 | return String.format("Both Servers called - Alice said '%s' & Tom replied '%s'.", serverAliceResponse, serverTomResponse); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client-bob/src/main/resources/alice-keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/client-bob/src/main/resources/alice-keystore.p12 -------------------------------------------------------------------------------- /client-bob/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 -------------------------------------------------------------------------------- /client-bob/src/main/resources/client-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/client-bob/src/main/resources/client-truststore.jks -------------------------------------------------------------------------------- /client-bob/src/main/resources/tom-keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/client-bob/src/main/resources/tom-keystore.p12 -------------------------------------------------------------------------------- /client-bob/src/test/java/de/jonashackt/BobTest.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.web.server.LocalServerPort; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.containsString; 11 | 12 | @SpringBootTest( 13 | classes = BobApplication.class, 14 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 15 | ) 16 | public class BobTest { 17 | 18 | @LocalServerPort 19 | private int port; 20 | 21 | private RestTemplate restTemplate = new RestTemplate(); 22 | 23 | @Disabled("no localhost support, because without Docker we would need to bind the same Port 8443 of server-alice and server-tom two times, which isn´t possible") 24 | @Test 25 | public void is_hello_resource_callable_with_client_cert() { 26 | String response = restTemplate.getForObject("https://localhost:" + port + "/secretservers", String.class); 27 | 28 | assertThat(response, containsString("Both Servers called - Alice said 'Alice answering!' & Tom replied 'Tom answering!'.")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client-truststore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/client-truststore.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | server-alice: 6 | build: ./server-alice 7 | ports: 8 | - "8443" 9 | tty: 10 | true 11 | restart: 12 | unless-stopped 13 | 14 | server-tom: 15 | build: ./server-tom 16 | ports: 17 | - "8443" 18 | tty: 19 | true 20 | restart: 21 | unless-stopped 22 | 23 | client-bob: 24 | build: ./client-bob 25 | ports: 26 | - "8080:8080" 27 | tty: 28 | true 29 | restart: 30 | unless-stopped -------------------------------------------------------------------------------- /docker-network-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | docker-network-client 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | 11 | de.jonashackt 12 | spring-boot-rest-clientcertificates-docker-compose 13 | 0.0.1-SNAPSHOT 14 | 15 | 16 | 17 | UTF-8 18 | 1.8 19 | 4.5.1 20 | 1.21.1 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | 37 | io.rest-assured 38 | rest-assured 39 | ${rest-assured.version} 40 | test 41 | 42 | 43 | 44 | org.testcontainers 45 | testcontainers 46 | ${testcontainers.version} 47 | test 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docker-network-client/src/test/java/de/jonashackt/ClientTest.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.apache.http.HttpStatus; 4 | import org.junit.ClassRule; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.test.context.ContextConfiguration; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import org.testcontainers.containers.DockerComposeContainer; 10 | import org.testcontainers.containers.wait.strategy.Wait; 11 | 12 | import java.io.File; 13 | 14 | import static io.restassured.RestAssured.when; 15 | import static org.hamcrest.Matchers.containsString; 16 | 17 | @RunWith(SpringRunner.class) 18 | @ContextConfiguration() 19 | public class ClientTest { 20 | 21 | @ClassRule 22 | public static DockerComposeContainer services = 23 | new DockerComposeContainer(new File("../docker-compose.yml")) 24 | .withExposedService("server-alice", 8443,Wait.forListeningPort()) 25 | .withExposedService("server-tom", 8443, Wait.forListeningPort()) 26 | .withExposedService("client-bob", 8080, Wait.forHttp("/swagger-ui.html").forStatusCode(200)); 27 | 28 | @Test 29 | public void is_client_bob_able_to_call_all_servers_with_client_certs() { 30 | 31 | when() 32 | .get("http://localhost:8080/secretservers") 33 | .then() 34 | .statusCode(HttpStatus.SC_OK) 35 | .assertThat() 36 | .body(containsString("Both Servers called - Alice said 'Alice answering!' & Tom replied 'Tom answering!'.")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | de.jonashackt 7 | spring-boot-rest-clientcertificates-docker-compose 8 | 0.0.1-SNAPSHOT 9 | pom 10 | 11 | Example project showing how to access REST endpoints from multiple servers that are secured by different client certificates with Spring´s RestTemplate 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 2.7.18 17 | 18 | 19 | 20 | 21 | ${project.basedir} 22 | 23 | 24 | 25 | server-alice 26 | server-tom 27 | client-bob 28 | docker-network-client 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "automergeType": "branch", 7 | "major": { 8 | "automerge": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server-alice/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | MAINTAINER Jonas Hecht 4 | 5 | VOLUME /tmp 6 | 7 | # Add Spring Boot app.jar to Container 8 | ADD "target/server-alice-0.0.1-SNAPSHOT.jar" app.jar 9 | 10 | ENV JAVA_OPTS="" 11 | 12 | # Fire up our Spring Boot app by default 13 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] -------------------------------------------------------------------------------- /server-alice/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | server-alice 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | 11 | de.jonashackt 12 | spring-boot-rest-clientcertificates-docker-compose 13 | 0.0.1-SNAPSHOT 14 | 15 | 16 | 17 | UTF-8 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-security 31 | 32 | 33 | 34 | 35 | org.apache.httpcomponents 36 | httpclient 37 | 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-test 43 | test 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-maven-plugin 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /server-alice/src/main/java/de/jonashackt/ServerAliceApplication.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ServerAliceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ServerAliceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server-alice/src/main/java/de/jonashackt/configuration/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.configuration; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Configuration 9 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 10 | 11 | /* 12 | * Enable x509 client authentication. 13 | */ 14 | @Override 15 | protected void configure(HttpSecurity http) throws Exception { 16 | http.x509(); 17 | } 18 | 19 | /* 20 | * Create an in-memory authentication manager. We create 1 user (localhost which 21 | * is the CN of the client certificate) which has a role of USER. 22 | */ 23 | @Override 24 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 25 | auth.inMemoryAuthentication() 26 | .withUser("localhost") 27 | .password("none") 28 | .roles("USER"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server-alice/src/main/java/de/jonashackt/controller/ServerAliceController.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.controller; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | public class ServerAliceController { 9 | 10 | public static final String RESPONSE = "Alice answering!"; 11 | 12 | @RequestMapping(path="/hello", method=RequestMethod.GET) 13 | public String helloWorld() { 14 | System.out.println("Alice´ Server was called"); 15 | return RESPONSE; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server-alice/src/main/resources/alice-csr.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | distinguished_name = req_distinguished_name 3 | req_extensions = v3_req 4 | prompt = no 5 | 6 | [req_distinguished_name] 7 | C = DE 8 | ST = Thuringia 9 | L = Erfurt 10 | O = Alice Corp 11 | OU = Team Foo 12 | CN = server-alice 13 | 14 | [v3_req] 15 | keyUsage = keyEncipherment, dataEncipherment 16 | extendedKeyUsage = serverAuth 17 | subjectAltName = @alt_names 18 | [alt_names] 19 | DNS.1 = server-alice 20 | DNS.2 = localhost 21 | -------------------------------------------------------------------------------- /server-alice/src/main/resources/alice-keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-alice/src/main/resources/alice-keystore.jks -------------------------------------------------------------------------------- /server-alice/src/main/resources/alice-keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-alice/src/main/resources/alice-keystore.p12 -------------------------------------------------------------------------------- /server-alice/src/main/resources/alice-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-alice/src/main/resources/alice-truststore.jks -------------------------------------------------------------------------------- /server-alice/src/main/resources/alice.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICqDCCAhGgAwIBAgIJAJfsRKTuMVkVMA0GCSqGSIb3DQEBBQUAMHExCzAJBgNV 3 | BAYTAkRFMRIwEAYDVQQIDAlUaHVyaW5naWExDzANBgNVBAcMBkVyZnVydDETMBEG 4 | A1UECgwKQWxpY2UgQ29ycDERMA8GA1UECwwIVGVhbSBGb28xFTATBgNVBAMMDHNl 5 | cnZlci1hbGljZTAeFw0xNzEyMTIxODEwMDhaFw0yNzEyMTAxODEwMDhaMHExCzAJ 6 | BgNVBAYTAkRFMRIwEAYDVQQIDAlUaHVyaW5naWExDzANBgNVBAcMBkVyZnVydDET 7 | MBEGA1UECgwKQWxpY2UgQ29ycDERMA8GA1UECwwIVGVhbSBGb28xFTATBgNVBAMM 8 | DHNlcnZlci1hbGljZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7WKw0oR+ 9 | WPiHfwUSm7OLyMKbdc1c08x47mrtJagHKhOmmTCLlZOZ6L1XzidgnF8dvG+7mODn 10 | oUjMDWAAmR65WLOV3KM5sZAnPmIK9/KzvO837tixEbrueQAC5IOYmdo9oc9rr5Vz 11 | iWzAa3xUtloEl2Jme1hdsq1AUtX35u0Ap3ECAwEAAaNIMEYwCwYDVR0PBAQDAgQw 12 | MBMGA1UdJQQMMAoGCCsGAQUFBwMBMCIGA1UdEQQbMBmCDHNlcnZlci1hbGljZYIJ 13 | bG9jYWxob3N0MA0GCSqGSIb3DQEBBQUAA4GBALyWWCPUJAwapV4g3qJ0PwATEkkd 14 | gC/nzm3J8KSsBBVAnzAn/IrWV1O3f+FDaxOMUyOXbYDql96okpKeohXjT/8Q1V+F 15 | iWWsBDvVS5q8OPlx0fdfk8bnEqUUa7T17WQLnfTl42f38pp/9p0sdIB3MVp2tRJZ 16 | p27l9wAt3E2dJDr1 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /server-alice/src/main/resources/alice.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICCDCCAXECAQAwcTELMAkGA1UEBhMCREUxEjAQBgNVBAgMCVRodXJpbmdpYTEP 3 | MA0GA1UEBwwGRXJmdXJ0MRMwEQYDVQQKDApBbGljZSBDb3JwMREwDwYDVQQLDAhU 4 | ZWFtIEZvbzEVMBMGA1UEAwwMc2VydmVyLWFsaWNlMIGfMA0GCSqGSIb3DQEBAQUA 5 | A4GNADCBiQKBgQDtYrDShH5Y+Id/BRKbs4vIwpt1zVzTzHjuau0lqAcqE6aZMIuV 6 | k5novVfOJ2CcXx28b7uY4OehSMwNYACZHrlYs5XcozmxkCc+Ygr38rO87zfu2LER 7 | uu55AALkg5iZ2j2hz2uvlXOJbMBrfFS2WgSXYmZ7WF2yrUBS1ffm7QCncQIDAQAB 8 | oFcwVQYJKoZIhvcNAQkOMUgwRjALBgNVHQ8EBAMCBDAwEwYDVR0lBAwwCgYIKwYB 9 | BQUHAwEwIgYDVR0RBBswGYIMc2VydmVyLWFsaWNlgglsb2NhbGhvc3QwDQYJKoZI 10 | hvcNAQELBQADgYEAb4ZuIj5jgAtryg2CQ1A6jAvkq96WolStj+iW7SvekgKZRNEJ 11 | CjvYrXD+0ysGu2VKCksCAAFNy/gQUWCBCvRZJ8VYD/m8ydFcsLDpZ/IfZ7OS5hy2 12 | A8KKmmcrDSTZHMCdt+7rmr+bVpjiy7iVakXxiwo/YoERTPtQcfXEliiHgIM= 13 | -----END CERTIFICATE REQUEST----- 14 | -------------------------------------------------------------------------------- /server-alice/src/main/resources/aliceprivate.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,2C075A54AADDF2B6 4 | 5 | /PhB7VsRJ+8nCCfZ8MAkI6HDRFhtqxqBaxlrCZEE1U7PKMVnjC/ABh59mB/Dtw/P 6 | WLGPfveYHfVqJLzMnrFuPlgbVaDGFmpry8gol1SBxFi7P2YowCcBilW2TYH2fe13 7 | 0vCsGY0sce8Pqu5u1TMUZ+AISGVrGrNyNdC99uy2j0XDWrwq3/RV+o4tAv4TdamW 8 | w4pVc6MpQkw8ywowmpbiWXiJdZyz5tDV4MTeUuFXErYTLIZBH7NZi0hgWjKYZXao 9 | HWOmX+EQWOw/MTJlUMfxQyuxZV9Ay8gxLOMJKuBZZbwcMvJzgcvtLpdVk6VhaDu7 10 | fjgm1sXtUDhmQsO6O6zYoDKFyI8AYVvN/5n5eXHygCCK5vOvXQYrff2htS3KHpjg 11 | 9qdMllVpNHJ/MxQ+c22h4Gu5Zsp3qr/+/91eBoEK9LRQT9Vk14HJaSNeH/Oj1hna 12 | pAuXSPW2F2IjyMnq3h87JMxjkYIHiQUBBOJCELCc63ihlMyWLXtnC1ZZh2oJc0KW 13 | LM1UCtgx3cLOxCAgRoi9OUeTyCLBUPxLWBxDVHLhFNa4ZXx+dHkY0/np5cBr7dGa 14 | 1ZxN4z1zNaUEFV4dDW3tV6S0onjvQJL6Rq6BmzRvZVV3BJIDiS51zHSW3q0cg4M1 15 | ZxwzM/tjszkDCk8CE9Rmr5/fxwYqVwDywUyEUIGwaCK/RDTY2mNYvNj2z/Ckr3Pw 16 | BugJMczZ3Gdf7qlGQriH2A7gy8iXg/aOoEAJSnxYTgdekNEr4uM0BGYhFEDJRbXl 17 | KJL1tpT0HwSwbvddqKuAHP4ZADq2iLr4ynp1ZF3VavQV66peDSVgog== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /server-alice/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8443 3 | ssl: 4 | key-store: classpath:alice-keystore.p12 5 | key-store-password: alicepassword 6 | trust-store: classpath:alice-truststore.jks 7 | trust-store-password: alicepassword 8 | client-auth: need 9 | security: 10 | headers: 11 | hsts: NONE -------------------------------------------------------------------------------- /server-alice/src/test/java/de/jonashackt/RestClientCertTest.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import de.jonashackt.controller.ServerAliceController; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.web.server.LocalServerPort; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | 14 | @SpringBootTest( 15 | classes = ServerAliceApplication.class, 16 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 17 | ) 18 | public class RestClientCertTest { 19 | 20 | @LocalServerPort 21 | private int port; 22 | 23 | @Autowired 24 | private RestTemplate restTemplate; 25 | 26 | @Test 27 | public void is_hello_resource_callable_with_client_cert() { 28 | String response = restTemplate.getForObject("https://localhost:" + port + "/hello", String.class); 29 | 30 | assertEquals(ServerAliceController.RESPONSE, response); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server-alice/src/test/java/de/jonashackt/RestClientCertTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.impl.client.HttpClients; 5 | import org.apache.http.ssl.SSLContextBuilder; 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 10 | import org.springframework.util.ResourceUtils; 11 | import org.springframework.web.client.RestTemplate; 12 | 13 | import javax.net.ssl.SSLContext; 14 | 15 | @Configuration 16 | public class RestClientCertTestConfiguration { 17 | 18 | private char[] alicePassword = "alicepassword".toCharArray(); 19 | 20 | @Bean 21 | public RestTemplate restTemplate(RestTemplateBuilder builder) throws Exception { 22 | 23 | SSLContext sslContext = SSLContextBuilder 24 | .create() 25 | .loadKeyMaterial(ResourceUtils.getFile("classpath:alice-keystore.p12"), alicePassword, alicePassword) 26 | .loadTrustMaterial(ResourceUtils.getFile("classpath:alice-truststore.jks"), alicePassword) 27 | .build(); 28 | 29 | HttpClient client = HttpClients.custom() 30 | .setSSLContext(sslContext) 31 | .build(); 32 | 33 | return builder 34 | .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(client)) 35 | .build(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server-alice/src/test/resources/alice-keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-alice/src/test/resources/alice-keystore.p12 -------------------------------------------------------------------------------- /server-alice/src/test/resources/alice-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-alice/src/test/resources/alice-truststore.jks -------------------------------------------------------------------------------- /server-tom/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | MAINTAINER Jonas Hecht 4 | 5 | VOLUME /tmp 6 | 7 | # Add Spring Boot app.jar to Container 8 | ADD "target/server-tom-0.0.1-SNAPSHOT.jar" app.jar 9 | 10 | ENV JAVA_OPTS="" 11 | 12 | # Fire up our Spring Boot app by default 13 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] -------------------------------------------------------------------------------- /server-tom/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | server-tom 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | 11 | de.jonashackt 12 | spring-boot-rest-clientcertificates-docker-compose 13 | 0.0.1-SNAPSHOT 14 | 15 | 16 | 17 | UTF-8 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-security 31 | 32 | 33 | 34 | 35 | org.apache.httpcomponents 36 | httpclient 37 | 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-test 43 | test 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-maven-plugin 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /server-tom/src/main/java/de/jonashackt/ServerTomApplication.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ServerTomApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ServerTomApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server-tom/src/main/java/de/jonashackt/configuration/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.configuration; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Configuration 9 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 10 | 11 | /* 12 | * Enable x509 client authentication. 13 | */ 14 | @Override 15 | protected void configure(HttpSecurity http) throws Exception { 16 | http.x509(); 17 | } 18 | 19 | /* 20 | * Create an in-memory authentication manager. We create 1 user (localhost which 21 | * is the CN of the client certificate) which has a role of USER. 22 | */ 23 | @Override 24 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 25 | auth.inMemoryAuthentication() 26 | .withUser("localhost") 27 | .password("none") 28 | .roles("USER"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server-tom/src/main/java/de/jonashackt/controller/ServerTomController.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.controller; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | public class ServerTomController { 9 | 10 | public static final String RESPONSE = "Tom answering!"; 11 | 12 | @RequestMapping(path="/hello", method=RequestMethod.GET) 13 | public String helloWorld() { 14 | System.out.println("Tom´s Server was called"); 15 | return RESPONSE; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server-tom/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8443 3 | ssl: 4 | key-store: classpath:tom-keystore.p12 5 | key-store-password: tompassword 6 | trust-store: classpath:tom-truststore.jks 7 | trust-store-password: tompassword 8 | client-auth: need 9 | security: 10 | headers: 11 | hsts: NONE -------------------------------------------------------------------------------- /server-tom/src/main/resources/tom-csr.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | distinguished_name = req_distinguished_name 3 | req_extensions = v3_req 4 | prompt = no 5 | 6 | [req_distinguished_name] 7 | C = DE 8 | ST = Thuringia 9 | L = Weimar 10 | O = Tom Inc. 11 | OU = Team Bar 12 | CN = server-tom 13 | 14 | [v3_req] 15 | keyUsage = keyEncipherment, dataEncipherment 16 | extendedKeyUsage = serverAuth 17 | subjectAltName = @alt_names 18 | [alt_names] 19 | DNS.1 = server-tom 20 | DNS.2 = localhost 21 | -------------------------------------------------------------------------------- /server-tom/src/main/resources/tom-keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-tom/src/main/resources/tom-keystore.p12 -------------------------------------------------------------------------------- /server-tom/src/main/resources/tom-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-tom/src/main/resources/tom-truststore.jks -------------------------------------------------------------------------------- /server-tom/src/main/resources/tom.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnjCCAgegAwIBAgIJAK5ZWgGKmkZ4MA0GCSqGSIb3DQEBBQUAMG0xCzAJBgNV 3 | BAYTAkRFMRIwEAYDVQQIDAlUaHVyaW5naWExDzANBgNVBAcMBldlaW1hcjERMA8G 4 | A1UECgwIVG9tIEluYy4xETAPBgNVBAsMCFRlYW0gQmFyMRMwEQYDVQQDDApzZXJ2 5 | ZXItdG9tMB4XDTE3MTIxMjE5MDcxNloXDTI3MTIxMDE5MDcxNlowbTELMAkGA1UE 6 | BhMCREUxEjAQBgNVBAgMCVRodXJpbmdpYTEPMA0GA1UEBwwGV2VpbWFyMREwDwYD 7 | VQQKDAhUb20gSW5jLjERMA8GA1UECwwIVGVhbSBCYXIxEzARBgNVBAMMCnNlcnZl 8 | ci10b20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMdyqaiG7hggBSUcJD2l 9 | SjR9V4KdNG5G2iH3nBM6hq8E9BwY9oJMg9rwru4AX9G27owdbhv2hbFxnUDotECL 10 | UuW+M6QYSMSAgNmRRgYxrXf4VvYm2/cEPgCKf/FizeiVQdRmjcEueOmxwb8hdoWy 11 | bwFXtluXbclgHQH481mFmdF3AgMBAAGjRjBEMAsGA1UdDwQEAwIEMDATBgNVHSUE 12 | DDAKBggrBgEFBQcDATAgBgNVHREEGTAXggpzZXJ2ZXItdG9tgglsb2NhbGhvc3Qw 13 | DQYJKoZIhvcNAQEFBQADgYEAEviefsSN+QuvIw6SXMGJqM+Q9KD6TwEobQCGuTlA 14 | LYfjbhatT6JbArOWdXQVGNsuSYSjz0OMIBwj5ldGqqSCxj9XSlsHypQRJusYsNh6 15 | xGcsdwTqHqA7XwncfoWG+nIiZ1n0wSA5LgVNl+0My39/UKtXo4NNeUrKrwKM8si8 16 | lT8= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /server-tom/src/main/resources/tom.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICAjCCAWsCAQAwbTELMAkGA1UEBhMCREUxEjAQBgNVBAgMCVRodXJpbmdpYTEP 3 | MA0GA1UEBwwGV2VpbWFyMREwDwYDVQQKDAhUb20gSW5jLjERMA8GA1UECwwIVGVh 4 | bSBCYXIxEzARBgNVBAMMCnNlcnZlci10b20wgZ8wDQYJKoZIhvcNAQEBBQADgY0A 5 | MIGJAoGBAMdyqaiG7hggBSUcJD2lSjR9V4KdNG5G2iH3nBM6hq8E9BwY9oJMg9rw 6 | ru4AX9G27owdbhv2hbFxnUDotECLUuW+M6QYSMSAgNmRRgYxrXf4VvYm2/cEPgCK 7 | f/FizeiVQdRmjcEueOmxwb8hdoWybwFXtluXbclgHQH481mFmdF3AgMBAAGgVTBT 8 | BgkqhkiG9w0BCQ4xRjBEMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggrBgEFBQcD 9 | ATAgBgNVHREEGTAXggpzZXJ2ZXItdG9tgglsb2NhbGhvc3QwDQYJKoZIhvcNAQEL 10 | BQADgYEAcH5ASI8FNrOSdvAwuWeqt2BmAycBHSLGLkj46AuynoUl7ppxhSGAS5Id 11 | 2hs7lf3heqBXoxFMfBxQeaxZFGBKkrZSUf4sROQzlY+IiplgiJIZ23xULXRmpzeZ 12 | X5dvv3JhBhj7TeBEn84K+ETQVs/k79ONfqNtwf1MvkaJf4AYKaE= 13 | -----END CERTIFICATE REQUEST----- 14 | -------------------------------------------------------------------------------- /server-tom/src/main/resources/tomprivate.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,7F219CD6B1FF6288 4 | 5 | r8ex7oDvrqu+bDR2X8S+VZ+7s+/ifZnVivJj2euUXmJ1B1DcD83b30CtpdF1955c 6 | G6hod+seAUwFfCVMEgli3tCP9suDrnzqdMfpDVr9iX++jcgDLfr7FqRfiTm5XqdF 7 | I20TEid0bFsjLlAXKEvU52im5nVXW4kWnI36kGZhmcTSQRrjtySVchqDnskxDP+l 8 | XPfmxKFLntoaYBdwTwcMsQYKsjZYH+zEhGP0UW8aALTn0Y4/2ZLm+vrMXzfiL+eY 9 | /0eTDVmffy1CadjYT9EP995oPK1tfgbUdD9g0i2z/4Ujy0ajECTgUB8KmGIL8Gf2 10 | I+jyG6zulTeTU2ELlv1xQalM0Mwnx0SD5abs0/n/jpgLH4DyAod0wcPvCqbBWPvU 11 | n1iDhApjshbRZ+4CbKleCWJ2inIKwD0GUbtB3h97CfGzU+ItWC2rX+qeCGtE5ac4 12 | DBAuyqrdkbHS/DxraiXUYjEYEEQX/NuTi6iUfEvGYsudybkQBEvkt6uMZg8HahS9 13 | fivQDojIWU6Y38qcFE3B5Yc1vIelYw5bwy1HHBsSzoLr3qKQ9rX1g580s7F3eonQ 14 | LlVJuN2xqfVqAenDvdECFsP/eoXVVWyj9ks/5IbHmENHEZ4oui6RxdzLjfM2nvVw 15 | naTPZt+ewAytYqxuzRink4+G9YWHqRJXkO95Dxpf79GdbiZlO7Bgu9s6if5ikC8/ 16 | ZSeH1aPWa1mmjfrRJoilcdV969jM/6W7jx/Wwh6j1h34jFtAwgPx8D77WkPDbOQP 17 | 5slc6yuLcUd2mznMiFmdIjCnspWM6RN4gc6aI9nytcdaT2a4oC7kkA== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /server-tom/src/test/java/de/jonashackt/RestClientCertTest.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import de.jonashackt.controller.ServerTomController; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.web.server.LocalServerPort; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | 15 | @SpringBootTest( 16 | classes = ServerTomApplication.class, 17 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 18 | ) 19 | public class RestClientCertTest { 20 | 21 | @LocalServerPort 22 | private int port; 23 | 24 | @Autowired 25 | private RestTemplate restTemplate; 26 | 27 | @Test 28 | public void is_hello_resource_callable_with_client_cert() { 29 | String response = restTemplate.getForObject("https://localhost:" + port + "/hello", String.class); 30 | 31 | assertEquals(ServerTomController.RESPONSE, response); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server-tom/src/test/java/de/jonashackt/RestClientCertTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.impl.client.HttpClients; 5 | import org.apache.http.ssl.SSLContextBuilder; 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 10 | import org.springframework.util.ResourceUtils; 11 | import org.springframework.web.client.RestTemplate; 12 | 13 | import javax.net.ssl.SSLContext; 14 | 15 | @Configuration 16 | public class RestClientCertTestConfiguration { 17 | 18 | private char[] tomPassword = "tompassword".toCharArray(); 19 | 20 | @Bean 21 | public RestTemplate restTemplate(RestTemplateBuilder builder) throws Exception { 22 | 23 | SSLContext sslContext = SSLContextBuilder 24 | .create() 25 | .loadKeyMaterial(ResourceUtils.getFile("classpath:tom-keystore.p12"), tomPassword, tomPassword) 26 | .loadTrustMaterial(ResourceUtils.getFile("classpath:tom-truststore.jks"), tomPassword) 27 | .build(); 28 | 29 | HttpClient client = HttpClients.custom() 30 | .setSSLContext(sslContext) 31 | .build(); 32 | 33 | return builder 34 | .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(client)) 35 | .build(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server-tom/src/test/resources/tom-keystore.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-tom/src/test/resources/tom-keystore.p12 -------------------------------------------------------------------------------- /server-tom/src/test/resources/tom-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/spring-boot-rest-clientcertificates-docker-compose/7ba4fe11631b9ab79c544239b5ad60739cb8e49d/server-tom/src/test/resources/tom-truststore.jks --------------------------------------------------------------------------------