Skip to content

Commit 09ddb27

Browse files
committed
Supports mTLS for config-server clients
1 parent 9be8ffb commit 09ddb27

File tree

2 files changed

+205
-4
lines changed

2 files changed

+205
-4
lines changed

spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/ConfigServerBindingsPropertiesProcessor.java

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,19 @@
1818

1919
import org.springframework.cloud.bindings.Binding;
2020
import org.springframework.cloud.bindings.Bindings;
21+
import org.springframework.cloud.bindings.boot.pem.PemSslStoreHelper;
2122
import org.springframework.core.env.Environment;
23+
import org.springframework.util.StringUtils;
2224

25+
import java.io.FileOutputStream;
26+
import java.io.IOException;
27+
import java.nio.file.Paths;
28+
import java.security.KeyStore;
29+
import java.security.KeyStoreException;
30+
import java.security.NoSuchAlgorithmException;
31+
import java.security.cert.CertificateException;
2332
import java.util.Map;
33+
import java.util.Random;
2434

2535
import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled;
2636

@@ -40,12 +50,63 @@ public void process(Environment environment, Bindings bindings, Map<String, Obje
4050
}
4151

4252
bindings.filterBindings(TYPE).forEach(binding -> {
43-
MapMapper map = new MapMapper(binding.getSecret(), properties);
53+
Map<String, String> secret = binding.getSecret();
54+
MapMapper map = new MapMapper(secret, properties);
4455
map.from("uri").to("spring.cloud.config.uri");
4556
map.from("client-id").to("spring.cloud.config.client.oauth2.clientId");
4657
map.from("client-secret").to("spring.cloud.config.client.oauth2.clientSecret");
4758
map.from("access-token-uri").to("spring.cloud.config.client.oauth2.accessTokenUri");
59+
60+
// When tls.crt and tls.key are set, enable mTLS for config client.
61+
String clientKey = secret.get("tls.key");
62+
String clientCert = secret.get("tls.crt");
63+
if (StringUtils.hasText(clientCert) != StringUtils.hasText(clientKey)) {
64+
throw new IllegalArgumentException("binding secret error: tls.key and tls.crt must both be set if either is set");
65+
}
66+
67+
if (clientKey != null && !clientKey.isEmpty()) {
68+
String generatedPassword = new Random().ints(97 /* letter a */, 122 /* letter z */ + 1)
69+
.limit(10)
70+
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
71+
.toString();
72+
73+
// Create a keystore
74+
String keyFilePath = createStore("client-keystore", generatedPassword, clientCert, clientKey, "config");
75+
76+
properties.put("spring.cloud.config.tls.enabled", true);
77+
properties.put("spring.cloud.config.tls.key-alias", "config");
78+
properties.put("spring.cloud.config.tls.key-store", "file:" + keyFilePath);
79+
properties.put("spring.cloud.config.tls.key-store-type", "PKCS12");
80+
properties.put("spring.cloud.config.tls.key-store-password", generatedPassword);
81+
properties.put("spring.cloud.config.tls.key-password", "");
82+
83+
String caCert = secret.get("ca.crt");
84+
if (caCert != null && !caCert.isEmpty()) {
85+
// Create a truststore from the CA cert
86+
String trustFilePath = createStore("client-truststore", generatedPassword, caCert, null, "ca");
87+
properties.put("spring.cloud.config.tls.trust-store", "file:" + trustFilePath);
88+
properties.put("spring.cloud.config.tls.trust-store-type", "PKCS12");
89+
properties.put("spring.cloud.config.tls.trust-store-password", generatedPassword);
90+
}
91+
}
4892
});
93+
}
4994

95+
private static String createStore(String name, String password, String certificate, String privateKey, String keyAlias) {
96+
String path = Paths.get(System.getProperty("java.io.tmpdir"), name + ".p12").toString();
97+
KeyStore store = PemSslStoreHelper.createKeyStore("key", "PKCS12", certificate, privateKey, keyAlias);
98+
99+
try (FileOutputStream fos = new FileOutputStream(path)) {
100+
store.store(fos, password.toCharArray());
101+
} catch (KeyStoreException e) {
102+
throw new IllegalStateException("Unable to write " + name, e);
103+
} catch (NoSuchAlgorithmException e) {
104+
throw new IllegalStateException("Cryptographic algorithm not available", e);
105+
} catch (CertificateException e) {
106+
throw new IllegalStateException("Unable to process certificate", e);
107+
} catch (IOException e) {
108+
throw new IllegalStateException("Unable to create " + name, e);
109+
}
110+
return path;
50111
}
51112
}

spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/ConfigServerBindingsPropertiesProcessorTest.java

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,28 @@
1616

1717
package org.springframework.cloud.bindings.boot;
1818

19+
import org.junit.jupiter.api.BeforeEach;
1920
import org.junit.jupiter.api.DisplayName;
2021
import org.junit.jupiter.api.Test;
2122
import org.springframework.cloud.bindings.Binding;
2223
import org.springframework.cloud.bindings.Bindings;
2324
import org.springframework.cloud.bindings.FluentMap;
25+
import org.springframework.core.io.ClassPathResource;
2426
import org.springframework.mock.env.MockEnvironment;
2527

28+
import java.io.File;
2629
import java.nio.file.Paths;
2730
import java.util.HashMap;
2831

2932
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
34+
import static org.junit.jupiter.api.Assertions.assertThrows;
3035
import static org.springframework.cloud.bindings.boot.ConfigServerBindingsPropertiesProcessor.TYPE;
3136

3237
@DisplayName("Config Server BindingsPropertiesProcessor")
3338
final class ConfigServerBindingsPropertiesProcessorTest {
3439

35-
private final Bindings bindings = new Bindings(
40+
private Bindings bindings = new Bindings(
3641
new Binding("test-name", Paths.get("test-path"),
3742
new FluentMap()
3843
.withEntry(Binding.TYPE, TYPE)
@@ -47,9 +52,20 @@ final class ConfigServerBindingsPropertiesProcessorTest {
4752

4853
private final HashMap<String, Object> properties = new HashMap<>();
4954

55+
private String cert;
56+
private String key;
57+
58+
@BeforeEach
59+
void fetchCerts() {
60+
assertDoesNotThrow(() -> {
61+
this.cert = TestHelper.resourceAsString(new ClassPathResource("pem/test-cert.pem"));
62+
this.key = TestHelper.resourceAsString(new ClassPathResource("pem/test-key.pem"));
63+
});
64+
}
65+
5066
@Test
5167
@DisplayName("contributes properties")
52-
void test() {
68+
void whenEnabled() {
5369
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
5470
assertThat(properties)
5571
.containsEntry("spring.cloud.config.uri", "test-uri")
@@ -60,12 +76,136 @@ void test() {
6076

6177
@Test
6278
@DisplayName("can be disabled")
63-
void disabled() {
79+
void whenDisabled() {
6480
environment.setProperty("org.springframework.cloud.bindings.boot.config.enable", "false");
6581

6682
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
6783

6884
assertThat(properties).isEmpty();
6985
}
7086

87+
@Test
88+
@DisplayName("contributes tls key-store properties when set")
89+
void whenKeystoreValuesSet() {
90+
bindings = new Bindings(
91+
new Binding("test-name", Paths.get("test-path"),
92+
new FluentMap()
93+
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
94+
.withEntry("tls.key", key)
95+
.withEntry("tls.crt", cert)
96+
)
97+
);
98+
99+
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
100+
101+
assertThat(properties)
102+
.containsEntry("spring.cloud.config.tls.enabled", true)
103+
.containsEntry("spring.cloud.config.tls.key-store-type", "PKCS12")
104+
.containsEntry("spring.cloud.config.tls.key-alias", "config")
105+
.containsKey("spring.cloud.config.tls.key-store")
106+
.containsKey("spring.cloud.config.tls.key-store-password")
107+
.containsKey("spring.cloud.config.tls.key-password")
108+
.doesNotContainKey("spring.cloud.config.tls.trust-store")
109+
.doesNotContainKey("spring.cloud.config.tls.trust-store-type")
110+
.doesNotContainKey("spring.cloud.config.tls.trust-store-password");
111+
112+
String path = properties.get("spring.cloud.config.tls.key-store").toString().substring(5);
113+
File f = new File(path);
114+
assertThat(f.exists()).isTrue();
115+
assertThat(f.isFile()).isTrue();
116+
}
117+
118+
@Test
119+
@DisplayName("contributes tls trust-store properties when set")
120+
void whenTruststoreValuesSet() {
121+
bindings = new Bindings(
122+
new Binding("test-name", Paths.get("test-path"),
123+
new FluentMap()
124+
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
125+
.withEntry("tls.key", key)
126+
.withEntry("tls.crt", cert)
127+
.withEntry("ca.crt", cert)
128+
)
129+
);
130+
131+
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
132+
133+
assertThat(properties)
134+
.containsEntry("spring.cloud.config.tls.enabled", true)
135+
.containsEntry("spring.cloud.config.tls.trust-store-type", "PKCS12")
136+
.containsKey("spring.cloud.config.tls.trust-store")
137+
.containsKey("spring.cloud.config.tls.trust-store-password");
138+
139+
String path = properties.get("spring.cloud.config.tls.trust-store").toString().substring(5);
140+
File f = new File(path);
141+
assertThat(f.exists()).isTrue();
142+
assertThat(f.isFile()).isTrue();
143+
}
144+
145+
@Test
146+
@DisplayName("throws when bad tls key-store values are set")
147+
void whenKeystoreValueIsNotValid() {
148+
bindings = new Bindings(
149+
new Binding("test-name", Paths.get("test-path"),
150+
new FluentMap()
151+
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
152+
.withEntry("tls.key", key)
153+
.withEntry("tls.crt", "this isn't a valid certificate")
154+
)
155+
);
156+
157+
assertThrows(IllegalStateException.class, () -> {
158+
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
159+
});
160+
}
161+
162+
@Test
163+
@DisplayName("throws when bad tls trust-store values are set")
164+
void whenTruststoreValueIsNotValid() {
165+
bindings = new Bindings(
166+
new Binding("test-name", Paths.get("test-path"),
167+
new FluentMap()
168+
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
169+
.withEntry("tls.key", key)
170+
.withEntry("tls.crt", cert)
171+
.withEntry("ca.crt", "this isn't a valid certificate")
172+
)
173+
);
174+
175+
assertThrows(IllegalStateException.class, () -> {
176+
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
177+
});
178+
}
179+
180+
@Test
181+
@DisplayName("throws when tls.crt is set but tls.key isn't")
182+
void whenCertificateSetWithoutPrivateKey() {
183+
bindings = new Bindings(
184+
new Binding("test-name", Paths.get("test-path"),
185+
new FluentMap()
186+
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
187+
.withEntry("tls.crt", cert)
188+
)
189+
);
190+
191+
assertThrows(IllegalArgumentException.class, () -> {
192+
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
193+
});
194+
}
195+
196+
@Test
197+
@DisplayName("throws when tls.key is set but tls.crt isn't")
198+
void whenPrivateKeySetWithoutCertificate() {
199+
bindings = new Bindings(
200+
new Binding("test-name", Paths.get("test-path"),
201+
new FluentMap()
202+
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
203+
.withEntry("tls.key", key)
204+
)
205+
);
206+
207+
assertThrows(IllegalArgumentException.class, () -> {
208+
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
209+
});
210+
}
71211
}

0 commit comments

Comments
 (0)