Supports mTLS for config-server clients (#109)

* Supports mTLS for config-server clients

* Common KeyStore codecleanup and refactoring

Signed-off-by: kvmw <mshamsi@vmware.com>

---------

Signed-off-by: kvmw <mshamsi@vmware.com>
This commit is contained in:
Kaveh Shamsi
2024-03-22 15:51:17 +01:00
committed by GitHub
parent 9be8ffbff9
commit e9b5e1609f
6 changed files with 272 additions and 75 deletions

View File

@@ -273,12 +273,21 @@ Disable Property: `org.springframework.cloud.bindings.boot.hana.enable`
Type: `config`
Disable Property: `org.springframework.cloud.bindings.boot.config.enable`
| Property | Value |
| -------------------------------------------------- | -------------------- |
| `spring.cloud.config.uri` | `{uri}` |
| `spring.cloud.config.client.oauth2.clientId` | `{client-id}` |
| `spring.cloud.config.client.oauth2.clientSecret` | `{client-secret}` |
| `spring.cloud.config.client.oauth2.accessTokenUri` | `{access-token-uri}` |
| Property | Value |
|------------------------------------------------------|--------------------------------------------------------|
| `spring.cloud.config.uri` | `{uri}` |
| `spring.cloud.config.client.oauth2.clientId` | `{client-id}` |
| `spring.cloud.config.client.oauth2.clientSecret` | `{client-secret}` |
| `spring.cloud.config.client.oauth2.accessTokenUri` | `{access-token-uri}` |
| `spring.cloud.config.tls.enabled` | `true` when `{tls.crt}` and `{tls.key}` are set |
| `spring.cloud.config.tls.key-store` | derived from `{tls.crt}` and `{tls.key}` |
| `spring.cloud.config.tls.key-store-type` | `"PKCS12"` when `{tls.crt}` and `{tls.key}` are set |
| `spring.cloud.config.tls.key-store-password` | random string when `{tls.crt}` and `{tls.key}` are set |
| `spring.cloud.config.tls.key-alias` | `"config"` when `{tls.crt}` and `{tls.key}` are set |
| `spring.cloud.config.tls.key-password` | `""` when `{tls.crt}` and `{tls.key}` are set |
| `spring.cloud.config.tls.trust-store` | derived from `{ca.crt}` when it is set |
| `spring.cloud.config.tls.trust-store-type` | `"PKCS12"` when `{ca.crt}` is set |
| `spring.cloud.config.tls.trust-store-password` | random string when `{ca.crt}` is set |
## SCS Eureka

View File

@@ -18,10 +18,14 @@ package org.springframework.cloud.bindings.boot;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.cloud.bindings.boot.pem.PemSslStoreHelper;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import java.nio.file.Path;
import java.util.Map;
import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled;
/**
@@ -40,12 +44,42 @@ final class ConfigServerBindingsPropertiesProcessor implements BindingsPropertie
}
bindings.filterBindings(TYPE).forEach(binding -> {
MapMapper map = new MapMapper(binding.getSecret(), properties);
Map<String, String> secret = binding.getSecret();
MapMapper map = new MapMapper(secret, properties);
map.from("uri").to("spring.cloud.config.uri");
map.from("client-id").to("spring.cloud.config.client.oauth2.clientId");
map.from("client-secret").to("spring.cloud.config.client.oauth2.clientSecret");
map.from("access-token-uri").to("spring.cloud.config.client.oauth2.accessTokenUri");
});
// When tls.crt and tls.key are set, enable mTLS for config client.
String clientKey = secret.get("tls.key");
String clientCert = secret.get("tls.crt");
if (StringUtils.hasText(clientCert) != StringUtils.hasText(clientKey)) {
throw new IllegalArgumentException("binding secret error: tls.key and tls.crt must both be set if either is set");
}
if (clientKey != null && !clientKey.isEmpty()) {
String generatedPassword = PemSslStoreHelper.generatePassword();
// Create a keystore
Path keyFilePath = PemSslStoreHelper.createKeyStoreFile("config-keystore", generatedPassword, clientCert, clientKey, "config");
properties.put("spring.cloud.config.tls.enabled", true);
properties.put("spring.cloud.config.tls.key-alias", "config");
properties.put("spring.cloud.config.tls.key-store", "file:" + keyFilePath);
properties.put("spring.cloud.config.tls.key-store-type", PemSslStoreHelper.PKCS12_STORY_TYPE);
properties.put("spring.cloud.config.tls.key-store-password", generatedPassword);
properties.put("spring.cloud.config.tls.key-password", "");
String caCert = secret.get("ca.crt");
if (caCert != null && !caCert.isEmpty()) {
// Create a truststore from the CA cert
Path trustFilePath = PemSslStoreHelper.createKeyStoreFile("config-truststore", generatedPassword, caCert, null, "ca");
properties.put("spring.cloud.config.tls.trust-store", "file:" + trustFilePath);
properties.put("spring.cloud.config.tls.trust-store-type", PemSslStoreHelper.PKCS12_STORY_TYPE);
properties.put("spring.cloud.config.tls.trust-store-password", generatedPassword);
}
}
});
}
}

View File

@@ -22,12 +22,8 @@ import org.springframework.core.env.Environment;
import org.springframework.cloud.bindings.boot.pem.PemSslStoreHelper;
import org.springframework.util.StringUtils;
import java.io.*;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Random;
import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled;
@@ -66,19 +62,14 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce
properties.put("eureka.instance.preferIpAddress", true);
}
Random random = new Random();
String generatedPassword = random.ints(97 /* letter a */, 122 /* letter z */ + 1)
.limit(10)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
String generatedPassword = PemSslStoreHelper.generatePassword();
// Create a trust store from the CA cert
String trustFilePath = Paths.get(System.getProperty("java.io.tmpdir"), "client-truststore.p12").toString();
KeyStore trustStore = PemSslStoreHelper.createKeyStore("trust", "PKCS12", caCert, null, "rootca");
createStoreFile("truststore", generatedPassword, trustFilePath, trustStore);
Path trustFilePath = PemSslStoreHelper.createKeyStoreFile("eureka-truststore", generatedPassword, caCert, null, "rootca");
properties.put("eureka.client.tls.enabled", true);
properties.put("eureka.client.tls.trust-store", "file:"+trustFilePath);
properties.put("eureka.client.tls.trust-store-type", "PKCS12");
properties.put("eureka.client.tls.trust-store-type", PemSslStoreHelper.PKCS12_STORY_TYPE);
properties.put("eureka.client.tls.trust-store-password", generatedPassword);
// When tls.crt and tls.key are set, enable mTLS for Eureka
@@ -90,41 +81,14 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce
if (clientKey != null && !clientKey.isEmpty()) {
// Create a keystore
String keyFilePath = Paths.get(System.getProperty("java.io.tmpdir"), "client-keystore.p12").toString();
KeyStore keyStore = PemSslStoreHelper.createKeyStore("key", "PKCS12", clientCert, clientKey, "eureka");
createStoreFile("keystore", generatedPassword, keyFilePath, keyStore);
Path keyFilePath = PemSslStoreHelper.createKeyStoreFile("eureka-keystore", generatedPassword, clientCert, clientKey, "eureka");
properties.put("eureka.client.tls.key-alias", "eureka");
properties.put("eureka.client.tls.key-store", "file:" + keyFilePath);
properties.put("eureka.client.tls.key-store-type", "PKCS12");
properties.put("eureka.client.tls.key-store-type", PemSslStoreHelper.PKCS12_STORY_TYPE);
properties.put("eureka.client.tls.key-store-password", generatedPassword);
properties.put("eureka.client.tls.key-password", "");
}
}
});
}
private static void createStoreFile(String storeType, String generatedPassword, String filePath, KeyStore ks) {
try {
FileOutputStream fos = new FileOutputStream(filePath);
try {
ks.store(fos, generatedPassword.toCharArray());
} catch (KeyStoreException e) {
throw new IllegalStateException("Unable to write " + storeType, e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Cryptographic algorithm not available", e);
} catch (CertificateException e) {
throw new IllegalStateException("Unable to process certificate", e);
} catch (IOException e) {
throw new IllegalStateException("Unable to create " + storeType, e);
} finally {
try {
fos.close();
} catch (IOException e) {
throw new IllegalStateException("Unable to close " + storeType + " output file", e);
}
}
} catch (FileNotFoundException e) {
throw new IllegalStateException("Unable to open " + storeType + " output file", e);
}
}
}

View File

@@ -16,34 +16,76 @@
package org.springframework.cloud.bindings.boot.pem;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Random;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* helper for creating stores from PEM-encoded certificates and private keys.
*/
public class PemSslStoreHelper {
public static final String PKCS12_STORY_TYPE = "PKCS12";
private static final String DEFAULT_KEY_ALIAS = "ssl";
/**
* Utility method to create a KeyStore
* @param name the name of the keystore
* @param storeType the type of the keystore (JKS, PKCS12, etc.)
* @param certificate a certificate as string that will be added to the keystore
* @param privateKey the keystore private key as string
* Utility method to create a KeyStore and save it in the tmp directory with give name.
* @param name the store file name
* @param password the store password
* @param certificate the certificate to add to the store
* @param privateKey the private key to add to the store
* @param keyAlias the alias
* @return the keystore
* @return the path which store file is saved
*/
public static KeyStore createKeyStore(String name, String storeType, String certificate, String privateKey, String keyAlias) {
public static Path createKeyStoreFile(String name, String password, String certificate, String privateKey, String keyAlias) {
KeyStore store = createKeyStore(certificate, privateKey, keyAlias);
Path path;
try {
path = Files.createTempFile(Paths.get(System.getProperty("java.io.tmpdir")), name, ".p12");
} catch (IOException e) {
throw new IllegalStateException("Unable to create " + name, e);
}
try (FileOutputStream fos = new FileOutputStream(path.toString())) {
store.store(fos, password.toCharArray());
} catch (KeyStoreException e) {
throw new IllegalStateException("Unable to write " + name, e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Cryptographic algorithm not available", e);
} catch (CertificateException e) {
throw new IllegalStateException("Unable to process certificate", e);
} catch (IOException e) {
throw new IllegalStateException("Unable to create " + name, e);
}
return path;
}
/**
* Generates a password to use for KeyStore and/or TrustStore
* @return the password
*/
public static String generatePassword() {
return new Random().ints(97 /* letter a */, 122 /* letter z */ + 1)
.limit(10)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
private static KeyStore createKeyStore(String certificate, String privateKey, String keyAlias) {
try {
Assert.notNull(certificate, "CertificateContent must not be null");
String type = StringUtils.hasText(storeType) ? storeType : KeyStore.getDefaultType();
KeyStore store = KeyStore.getInstance(type);
KeyStore store = KeyStore.getInstance(PKCS12_STORY_TYPE);
store.load(null);
String certificateContent = PemContent.load(certificate);
String privateKeyContent = PemContent.load(privateKey);
@@ -53,7 +95,7 @@ public class PemSslStoreHelper {
return store;
}
catch (Exception ex) {
throw new IllegalStateException(String.format("Unable to create %s store: %s", name, ex.getMessage()), ex);
throw new IllegalStateException(String.format("Unable to create key/trust store: %s", ex.getMessage()), ex);
}
}

View File

@@ -16,23 +16,28 @@
package org.springframework.cloud.bindings.boot;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.cloud.bindings.FluentMap;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.env.MockEnvironment;
import java.io.File;
import java.nio.file.Paths;
import java.util.HashMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.cloud.bindings.boot.ConfigServerBindingsPropertiesProcessor.TYPE;
@DisplayName("Config Server BindingsPropertiesProcessor")
final class ConfigServerBindingsPropertiesProcessorTest {
private final Bindings bindings = new Bindings(
private Bindings bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, TYPE)
@@ -47,9 +52,20 @@ final class ConfigServerBindingsPropertiesProcessorTest {
private final HashMap<String, Object> properties = new HashMap<>();
private String cert;
private String key;
@BeforeEach
void fetchCerts() {
assertDoesNotThrow(() -> {
this.cert = TestHelper.resourceAsString(new ClassPathResource("pem/test-cert.pem"));
this.key = TestHelper.resourceAsString(new ClassPathResource("pem/test-key.pem"));
});
}
@Test
@DisplayName("contributes properties")
void test() {
void whenEnabled() {
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties)
.containsEntry("spring.cloud.config.uri", "test-uri")
@@ -60,7 +76,7 @@ final class ConfigServerBindingsPropertiesProcessorTest {
@Test
@DisplayName("can be disabled")
void disabled() {
void whenDisabled() {
environment.setProperty("org.springframework.cloud.bindings.boot.config.enable", "false");
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
@@ -68,4 +84,128 @@ final class ConfigServerBindingsPropertiesProcessorTest {
assertThat(properties).isEmpty();
}
@Test
@DisplayName("contributes tls key-store properties when set")
void whenKeystoreValuesSet() {
bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
.withEntry("tls.key", key)
.withEntry("tls.crt", cert)
)
);
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties)
.containsEntry("spring.cloud.config.tls.enabled", true)
.containsEntry("spring.cloud.config.tls.key-store-type", "PKCS12")
.containsEntry("spring.cloud.config.tls.key-alias", "config")
.containsKey("spring.cloud.config.tls.key-store")
.containsKey("spring.cloud.config.tls.key-store-password")
.containsKey("spring.cloud.config.tls.key-password")
.doesNotContainKey("spring.cloud.config.tls.trust-store")
.doesNotContainKey("spring.cloud.config.tls.trust-store-type")
.doesNotContainKey("spring.cloud.config.tls.trust-store-password");
String path = properties.get("spring.cloud.config.tls.key-store").toString().substring(5);
File f = new File(path);
assertThat(f.exists()).isTrue();
assertThat(f.isFile()).isTrue();
}
@Test
@DisplayName("contributes tls trust-store properties when set")
void whenTruststoreValuesSet() {
bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
.withEntry("tls.key", key)
.withEntry("tls.crt", cert)
.withEntry("ca.crt", cert)
)
);
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties)
.containsEntry("spring.cloud.config.tls.enabled", true)
.containsEntry("spring.cloud.config.tls.trust-store-type", "PKCS12")
.containsKey("spring.cloud.config.tls.trust-store")
.containsKey("spring.cloud.config.tls.trust-store-password");
String path = properties.get("spring.cloud.config.tls.trust-store").toString().substring(5);
File f = new File(path);
assertThat(f.exists()).isTrue();
assertThat(f.isFile()).isTrue();
}
@Test
@DisplayName("throws when bad tls key-store values are set")
void whenKeystoreValueIsNotValid() {
bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
.withEntry("tls.key", key)
.withEntry("tls.crt", "this isn't a valid certificate")
)
);
assertThrows(IllegalStateException.class, () -> {
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
});
}
@Test
@DisplayName("throws when bad tls trust-store values are set")
void whenTruststoreValueIsNotValid() {
bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
.withEntry("tls.key", key)
.withEntry("tls.crt", cert)
.withEntry("ca.crt", "this isn't a valid certificate")
)
);
assertThrows(IllegalStateException.class, () -> {
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
});
}
@Test
@DisplayName("throws when tls.crt is set but tls.key isn't")
void whenCertificateSetWithoutPrivateKey() {
bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
.withEntry("tls.crt", cert)
)
);
assertThrows(IllegalArgumentException.class, () -> {
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
});
}
@Test
@DisplayName("throws when tls.key is set but tls.crt isn't")
void whenPrivateKeySetWithoutCertificate() {
bindings = new Bindings(
new Binding("test-name", Paths.get("test-path"),
new FluentMap()
.withEntry(Binding.TYPE, ConfigServerBindingsPropertiesProcessor.TYPE)
.withEntry("tls.key", key)
)
);
assertThrows(IllegalArgumentException.class, () -> {
new ConfigServerBindingsPropertiesProcessor().process(environment, bindings, properties);
});
}
}

View File

@@ -16,10 +16,8 @@
package org.springframework.cloud.bindings.boot.pem;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import org.junit.jupiter.api.Test;
@@ -33,15 +31,16 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
*/
class PemSslStoreHelperTests {
@Test
void whenNullValues() {
void createKeyStoreFileWhenNullValues() {
assertThrows(java.lang.IllegalStateException.class, () -> {
PemSslStoreHelper.createKeyStore("key", "PKCS12", null, null, "some-alias");
PemSslStoreHelper.createKeyStoreFile("key", "secret",null, null, "some-alias");
});
}
@Test
void whenHasKeyStoreDetailsCertAndKey() {
KeyStore keyStore = PemSslStoreHelper.createKeyStore("key", "PKCS12", "classpath:pem/test-cert.pem", "classpath:pem/test-key.pem", "some-alias");
void createKeyStoreFileWhenHasKeyStoreDetailsCertAndKey() throws Exception {
Path path = PemSslStoreHelper.createKeyStoreFile("key", "secret", "classpath:pem/test-cert.pem", "classpath:pem/test-key.pem", "some-alias");
KeyStore keyStore = KeyStore.getInstance(path.toFile(), "secret".toCharArray());
assertDoesNotThrow(() -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo("PKCS12");
@@ -52,9 +51,10 @@ class PemSslStoreHelperTests {
}
@Test
void whenHasTrustStoreDetailsWithoutKey() throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
KeyStore keyStore = PemSslStoreHelper.createKeyStore("trust", "PKCS12", "classpath:pem/test-cert.pem", null, null);
assertDoesNotThrow(() -> {
void createKeyStoreFileWhenHasTrustStoreDetailsWithoutKey() throws Exception {
Path path = PemSslStoreHelper.createKeyStoreFile("trust", "secret", "classpath:pem/test-cert.pem", null, null);
KeyStore keyStore = KeyStore.getInstance(path.toFile(), "secret".toCharArray());
assertDoesNotThrow(() -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo("PKCS12");
assertThat(keyStore.containsAlias("ssl-0")).isTrue();
@@ -62,4 +62,12 @@ class PemSslStoreHelperTests {
assertThat(keyStore.getKey("ssl-0", new char[]{})).isNull();
});
}
@Test
void generatePassword() {
String s = PemSslStoreHelper.generatePassword();
assertThat(s).isNotNull();
assertThat(s.length()).isEqualTo(10);
System.out.println(s);
}
}