Merge remote-tracking branch 'origin/2.2.x'

This commit is contained in:
Ryan Baxter
2020-08-24 16:23:00 -04:00
14 changed files with 899 additions and 3 deletions

View File

@@ -1611,6 +1611,28 @@ spring:
----
If config server requires client side TLS certificate, you can configure client side TLS certificate and trust store via properties, as shown in following example:
.bootstrap.yml
[source,yaml]
----
spring:
cloud:
config:
uri: https://myconfig.myconfig.com
tls:
enabled: true
key-store: <path-of-key-store>
key-store-type: PKCS12
key-store-password: <key-store-password>
key-password: <key-password>
trust-store: <path-of-trust-store>
trust-store-type: PKCS12
trust-store-password: <trust-store-password>
----
The `spring.cloud.config.tls.enabled` needs to be true to enable config client side TLS. When `spring.cloud.config.tls.trust-store` is omitted, a JVM default trust store is used. The default value for `spring.cloud.config.tls.key-store-type` and `spring.cloud.config.tls.trust-store-type` is PKCS12. When password properties are omitted, empty password is assumed.
If you use another form of security, you might need to <<custom-rest-template,provide a `RestTemplate`>> to the `ConfigServicePropertySourceLocator` (for example, by grabbing it in the bootstrap context and injecting it).
==== Health Indicator

View File

@@ -43,6 +43,7 @@
<module>spring-cloud-config-monitor</module>
<module>spring-cloud-config-sample</module>
<module>spring-cloud-starter-config</module>
<module>spring-cloud-config-client-tls-tests</module>
<module>docs</module>
</modules>
<dependencyManagement>

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-config-client-tls-tests</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Config Client TLS Tests</name>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>2.2.5.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<url>https://spring.io</url>
<description>
<![CDATA[
This project is a Spring configuration client.
]]>
</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.64</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.SocketUtils;
public class AppRunner implements AutoCloseable {
private Class<?> appClass;
private Map<String, String> props;
private ConfigurableApplicationContext app;
public AppRunner(Class<?> appClass) {
this.appClass = appClass;
props = new LinkedHashMap<>();
}
public void property(String key, String value) {
props.put(key, value);
}
public void start() {
if (app == null) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(appClass);
builder.properties("spring.jmx.enabled=false");
builder.properties(String.format("server.port=%d", availabeTcpPort()));
builder.properties(props());
app = builder.build().run();
}
}
private int availabeTcpPort() {
return SocketUtils.findAvailableTcpPort();
}
private String[] props() {
List<String> result = new ArrayList<>();
for (String key : props.keySet()) {
String value = props.get(key);
result.add(String.format("%s=%s", key, value));
}
return result.toArray(new String[0]);
}
public void stop() {
if (app != null) {
app.stop();
app = null;
}
}
public ConfigurableApplicationContext app() {
return app;
}
public String getProperty(String key) {
return app.getEnvironment().getProperty(key);
}
public <T> T getBean(Class<T> type) {
return app.getBean(type);
}
public ApplicationContext parent() {
return app.getParent();
}
public <T> Map<String, T> getParentBeans(Class<T> type) {
return parent().getBeansOfType(type);
}
public int port() {
if (app == null) {
throw new RuntimeException("App is not running.");
}
return app.getEnvironment().getProperty("server.port", Integer.class, -1);
}
public String root() {
if (app == null) {
throw new RuntimeException("App is not running.");
}
String protocol = tlsEnabled() ? "https" : "http";
return String.format("%s://localhost:%d/", protocol, port());
}
private boolean tlsEnabled() {
return app.getEnvironment().getProperty("server.ssl.enabled", Boolean.class,
false);
}
@Override
public void close() {
stop();
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.security.KeyStore;
import org.junit.BeforeClass;
public class BaseCertTest {
protected static final String KEY_STORE_PASSWORD = "test-key-store-password";
protected static final String KEY_PASSWORD = "test-key-password";
protected static final String WRONG_PASSWORD = "test-wrong-password";
protected static File caCert;
protected static File wrongCaCert;
protected static File serverCert;
protected static File clientCert;
protected static File wrongClientCert;
@BeforeClass
public static void createCertificates() throws Exception {
KeyTool tool = new KeyTool();
KeyAndCert ca = tool.createCA("MyCA");
KeyAndCert server = ca.sign("server");
KeyAndCert client = ca.sign("client");
caCert = saveCert(ca);
serverCert = saveKeyAndCert(server);
clientCert = saveKeyAndCert(client);
KeyAndCert wrongCa = tool.createCA("WrongCA");
KeyAndCert wrongClient = wrongCa.sign("client");
wrongCaCert = saveCert(wrongCa);
wrongClientCert = saveKeyAndCert(wrongClient);
System.setProperty("javax.net.ssl.trustStore", caCert.getAbsolutePath());
System.setProperty("javax.net.ssl.trustStorePassword", KEY_STORE_PASSWORD);
}
private static File saveKeyAndCert(KeyAndCert keyCert) throws Exception {
return saveKeyStore(keyCert.subject(),
() -> keyCert.storeKeyAndCert(KEY_PASSWORD));
}
private static File saveCert(KeyAndCert keyCert) throws Exception {
return saveKeyStore(keyCert.subject(), () -> keyCert.storeCert());
}
private static File saveKeyStore(String prefix, KeyStoreSupplier func)
throws Exception {
File result = File.createTempFile(prefix, ".p12");
result.deleteOnExit();
try (OutputStream output = new FileOutputStream(result)) {
KeyStore store = func.createKeyStore();
store.store(output, KEY_STORE_PASSWORD.toCharArray());
}
return result;
}
interface KeyStoreSupplier {
KeyStore createKeyStore() throws Exception;
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.io.File;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import static org.assertj.core.api.Assertions.assertThat;
public class ConfigClientTest extends BaseCertTest {
private static TlsConfigServerRunner server;
@BeforeClass
public static void setupAll() throws Exception {
startConfigServer();
}
@AfterClass
public static void tearDownAll() {
stopConfigServer();
}
private static void startConfigServer() {
server = new TlsConfigServerRunner(TestConfigServer.class);
server.enableTls();
server.setKeyStore(serverCert, KEY_STORE_PASSWORD, "server", KEY_PASSWORD);
server.setTrustStore(caCert, KEY_STORE_PASSWORD);
server.start();
}
private static void stopConfigServer() {
server.stop();
}
@Test
public void clientCertCanWork() {
try (TlsConfigClientRunner client = createConfigClient()) {
enableTlsClient(client);
client.start();
assertThat(client.getProperty("dumb.key")).isEqualTo("dumb-value");
}
}
@Test
public void tlsClientCanBeDisabled() {
try (TlsConfigClientRunner client = createConfigClient()) {
enableTlsClient(client);
client.property("spring.cloud.config.tls.enabled", "false");
client.start();
assertThat(client.getProperty("dumb.key")).isNull();
}
}
@Test
public void noCertCannotWork() {
try (TlsConfigClientRunner client = createConfigClient()) {
client.disableTls();
client.start();
assertThat(client.getProperty("dumb.key")).isNull();
}
}
@Test
public void wrongCertCannotWork() {
try (TlsConfigClientRunner client = createConfigClient()) {
enableTlsClient(client);
client.setKeyStore(wrongClientCert);
client.start();
assertThat(client.getProperty("dumb.key")).isNull();
}
}
@Test(expected = IllegalStateException.class)
public void wrongPasswordCauseFailure() {
TlsConfigClientRunner client = createConfigClient();
enableTlsClient(client);
client.setKeyStore(clientCert, WRONG_PASSWORD, WRONG_PASSWORD);
client.start();
}
@Test(expected = IllegalStateException.class)
public void nonExistKeyStoreCauseFailure() {
TlsConfigClientRunner client = createConfigClient();
enableTlsClient(client);
client.setKeyStore(new File("nonExistFile"));
client.start();
}
@Test
public void wrongTrustStoreCannotWork() {
try (TlsConfigClientRunner client = createConfigClient()) {
enableTlsClient(client);
client.setTrustStore(wrongCaCert);
client.start();
assertThat(client.getProperty("dumb.key")).isNull();
}
}
private TlsConfigClientRunner createConfigClient() {
return new TlsConfigClientRunner(TestApp.class, server);
}
private void enableTlsClient(TlsConfigClientRunner runner) {
runner.enableTls();
runner.setKeyStore(clientCert, KEY_STORE_PASSWORD, KEY_PASSWORD);
runner.setTrustStore(caCert, KEY_STORE_PASSWORD);
}
@SpringBootConfiguration
@EnableAutoConfiguration
public static class TestApp {
}
@SpringBootConfiguration
@EnableAutoConfiguration
@EnableConfigServer
public static class TestConfigServer {
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
public class KeyAndCert {
private KeyPair keyPair;
private X509Certificate certificate;
public KeyAndCert(KeyPair keyPair, X509Certificate certificate) {
this.keyPair = keyPair;
this.certificate = certificate;
}
public KeyPair keyPair() {
return keyPair;
}
public PublicKey publicKey() {
return keyPair.getPublic();
}
public PrivateKey privateKey() {
return keyPair.getPrivate();
}
public X509Certificate certificate() {
return certificate;
}
public String subject() {
String dn = certificate.getSubjectDN().getName();
int index = dn.indexOf('=');
return dn.substring(index + 1);
}
public KeyAndCert sign(String subject) throws Exception {
KeyTool tool = new KeyTool();
return tool.signCertificate(subject, this);
}
public KeyAndCert sign(KeyPair keyPair, String subject) throws Exception {
KeyTool tool = new KeyTool();
return tool.signCertificate(keyPair, subject, this);
}
public KeyStore storeKeyAndCert(String keyPassword) throws Exception {
KeyStore result = KeyStore.getInstance("PKCS12");
result.load(null);
result.setKeyEntry(subject(), keyPair.getPrivate(), keyPassword.toCharArray(),
certChain());
return result;
}
private Certificate[] certChain() {
return new Certificate[] { certificate() };
}
public KeyStore storeCert() throws Exception {
return storeCert("PKCS12");
}
public KeyStore storeCert(String storeType) throws Exception {
KeyStore result = KeyStore.getInstance(storeType);
result.load(null);
result.setCertificateEntry(subject(), certificate());
return result;
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
public class KeyTool {
private static final long ONE_DAY = 1000L * 60L * 60L * 24L;
private static final long TEN_YEARS = ONE_DAY * 365L * 10L;
public KeyAndCert createCA(String ca) throws Exception {
KeyPair keyPair = createKeyPair();
X509Certificate certificate = createCert(keyPair, ca);
return new KeyAndCert(keyPair, certificate);
}
public KeyAndCert signCertificate(String subject, KeyAndCert signer)
throws Exception {
return signCertificate(createKeyPair(), subject, signer);
}
public KeyAndCert signCertificate(KeyPair keyPair, String subject, KeyAndCert signer)
throws Exception {
X509Certificate certificate = createCert(keyPair.getPublic(), signer.privateKey(),
signer.subject(), subject);
KeyAndCert result = new KeyAndCert(keyPair, certificate);
return result;
}
public KeyPair createKeyPair() throws Exception {
return createKeyPair(1024);
}
public KeyPair createKeyPair(int keySize) throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(keySize, new SecureRandom());
return gen.generateKeyPair();
}
public X509Certificate createCert(KeyPair keyPair, String ca) throws Exception {
JcaX509v3CertificateBuilder builder = certBuilder(keyPair.getPublic(), ca, ca);
builder.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.keyCertSign));
builder.addExtension(Extension.basicConstraints, false,
new BasicConstraints(true));
return signCert(builder, keyPair.getPrivate());
}
public X509Certificate createCert(PublicKey publicKey, PrivateKey privateKey,
String issuer, String subject) throws Exception {
JcaX509v3CertificateBuilder builder = certBuilder(publicKey, issuer, subject);
builder.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature));
builder.addExtension(Extension.basicConstraints, false,
new BasicConstraints(false));
GeneralName[] names = new GeneralName[] {
new GeneralName(GeneralName.dNSName, "localhost") };
builder.addExtension(Extension.subjectAlternativeName, false,
GeneralNames.getInstance(new DERSequence(names)));
return signCert(builder, privateKey);
}
private JcaX509v3CertificateBuilder certBuilder(PublicKey publicKey, String issuer,
String subject) {
X500Name issuerName = new X500Name(String.format("dc=%s", issuer));
X500Name subjectName = new X500Name(String.format("dc=%s", subject));
long now = System.currentTimeMillis();
BigInteger serialNum = BigInteger.valueOf(now);
Date notBefore = new Date(now - ONE_DAY);
Date notAfter = new Date(now + TEN_YEARS);
return new JcaX509v3CertificateBuilder(issuerName, serialNum, notBefore, notAfter,
subjectName, publicKey);
}
private X509Certificate signCert(JcaX509v3CertificateBuilder builder,
PrivateKey privateKey) throws Exception {
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA")
.build(privateKey);
X509CertificateHolder holder = builder.build(signer);
return new JcaX509CertificateConverter().getCertificate(holder);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.io.File;
public class TlsConfigClientRunner extends AppRunner {
public TlsConfigClientRunner(Class<?> appClass, AppRunner server) {
super(appClass);
property("spring.cloud.config.uri", server.root());
property("spring.cloud.config.enabled", "true");
}
public void enableTls() {
property("spring.cloud.config.tls.enabled", "true");
}
public void disableTls() {
property("spring.cloud.config.tls.enabled", "false");
}
public void setKeyStore(File keyStore, String keyStorePassword, String keyPassword) {
property("spring.cloud.config.tls.key-store", pathOf(keyStore));
property("spring.cloud.config.tls.key-store-password", keyStorePassword);
property("spring.cloud.config.tls.key-password", keyPassword);
}
public void setKeyStore(File keyStore) {
property("spring.cloud.config.tls.key-store", pathOf(keyStore));
}
public void setTrustStore(File trustStore, String password) {
property("spring.cloud.config.tls.trust-store", pathOf(trustStore));
property("spring.cloud.config.tls.trust-store-password", password);
}
public void setTrustStore(File trustStore) {
property("spring.cloud.config.tls.trust-store", pathOf(trustStore));
}
private String pathOf(File file) {
return String.format("file:%s", file.getAbsolutePath());
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.io.File;
public class TlsConfigServerRunner extends AppRunner {
public TlsConfigServerRunner(Class<?> appClass) {
super(appClass);
property("spring.profiles.active", "native");
property("spring.cloud.config.server.native.search-locations",
"classpath:/test/config");
}
public void enableTls() {
property("server.ssl.enabled", "true");
property("server.ssl.client-auth", "need");
}
public void setKeyStore(File keyStore, String keyStorePassword, String key,
String keyPassword) {
property("server.ssl.key-store", pathOf(keyStore));
property("server.ssl.key-store-type", "PKCS12");
property("server.ssl.key-store-password", keyStorePassword);
property("server.ssl.key-alias", key);
property("server.ssl.key-password", keyPassword);
}
public void setTrustStore(File trustStore, String password) {
property("server.ssl.trust-store", pathOf(trustStore));
property("server.ssl.trust-store-type", "PKCS12");
property("server.ssl.trust-store-password", password);
}
private String pathOf(File file) {
return String.format("file:%s", file.getAbsolutePath());
}
}

View File

@@ -0,0 +1 @@
dumb.key=dumb-value

View File

@@ -71,6 +71,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>

View File

@@ -22,9 +22,12 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.configuration.TlsProperties;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
@@ -99,6 +102,11 @@ public class ConfigClientProperties {
*/
private Discovery discovery = new Discovery();
/**
* TLS properties.
*/
private TlsProperties tls = new TlsProperties();
/**
* Flag to indicate that failure to connect to the server is fatal (default false).
*/
@@ -208,6 +216,19 @@ public class ConfigClientProperties {
this.discovery = discovery;
}
public TlsProperties getTls() {
return tls;
}
public void setTls(TlsProperties tls) {
this.tls = tls;
}
@PostConstruct
public void checkTlsStoreType() {
tls.postConstruct();
}
public boolean isFailFast() {
return this.failFast;
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.config.client;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -26,8 +27,12 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.net.ssl.SSLContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.Origin;
@@ -37,6 +42,7 @@ import org.springframework.cloud.bootstrap.support.OriginTrackedCompositePropert
import org.springframework.cloud.config.client.ConfigClientProperties.Credentials;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.cloud.configuration.SSLContextFactory;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.MapPropertySource;
@@ -48,8 +54,10 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.retry.annotation.Retryable;
import org.springframework.util.Assert;
@@ -296,15 +304,14 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
if (client.getRequestReadTimeout() < 0) {
throw new IllegalStateException("Invalid Value for Read Timeout set.");
}
if (client.getRequestConnectTimeout() < 0) {
throw new IllegalStateException("Invalid Value for Connect Timeout set.");
}
requestFactory.setReadTimeout(client.getRequestReadTimeout());
requestFactory.setConnectTimeout(client.getRequestConnectTimeout());
ClientHttpRequestFactory requestFactory = createHttpRquestFactory(client);
RestTemplate template = new RestTemplate(requestFactory);
Map<String, String> headers = new HashMap<>(client.getHeaders());
if (headers.containsKey(AUTHORIZATION)) {
@@ -318,6 +325,35 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
return template;
}
private ClientHttpRequestFactory createHttpRquestFactory(
ConfigClientProperties client) {
if (client.getTls().isEnabled()) {
try {
SSLContextFactory factory = new SSLContextFactory(client.getTls());
SSLContext sslContext = factory.createSSLContext();
HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext)
.build();
HttpComponentsClientHttpRequestFactory result = new HttpComponentsClientHttpRequestFactory(
httpClient);
result.setReadTimeout(client.getRequestReadTimeout());
result.setConnectTimeout(client.getRequestConnectTimeout());
return result;
}
catch (GeneralSecurityException | IOException ex) {
logger.error(ex);
throw new IllegalStateException(
"Failed to create config client with TLS.", ex);
}
}
SimpleClientHttpRequestFactory result = new SimpleClientHttpRequestFactory();
result.setReadTimeout(client.getRequestReadTimeout());
result.setConnectTimeout(client.getRequestConnectTimeout());
return result;
}
private void addAuthorizationToken(ConfigClientProperties configClientProperties,
HttpHeaders httpHeaders, String username, String password) {
String authorization = configClientProperties.getHeaders().get(AUTHORIZATION);