diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 3ae77fa4ec..574e525e03 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -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: + key-store-type: PKCS12 + key-store-password: + key-password: + trust-store: + trust-store-type: PKCS12 + 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 <> to the `ConfigServicePropertySourceLocator` (for example, by grabbing it in the bootstrap context and injecting it). ==== Health Indicator diff --git a/pom.xml b/pom.xml index ba4aaa95e4..b76306f418 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ 2.2.5.BUILD-SNAPSHOT 1.11.52 v1-rev20191010-1.30.3 + 1.64 true true @@ -43,6 +44,7 @@ spring-cloud-config-monitor spring-cloud-config-sample spring-cloud-starter-config + spring-cloud-config-client-tls-tests docs @@ -87,6 +89,11 @@ google-auth-library-oauth2-http 0.15.0 + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + diff --git a/spring-cloud-config-client-tls-tests/pom.xml b/spring-cloud-config-client-tls-tests/pom.xml new file mode 100644 index 0000000000..569ca1ba95 --- /dev/null +++ b/spring-cloud-config-client-tls-tests/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + spring-cloud-config-client-tls-tests + jar + Spring Cloud Config Client TLS Tests + + + org.springframework.cloud + spring-cloud-config + 2.2.5.BUILD-SNAPSHOT + .. + + + https://spring.io + + + + + + + org.springframework.cloud + spring-cloud-config-server + ${project.version} + + + org.springframework.cloud + spring-cloud-config-client + ${project.version} + + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-logging + true + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + org.springframework + spring-web + + + com.fasterxml.jackson.core + jackson-annotations + + + org.springframework.retry + spring-retry + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-starter-aop + true + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.vintage + junit-vintage-engine + test + + + org.bouncycastle + bcpkix-jdk15on + test + + + + diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/AppRunner.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/AppRunner.java new file mode 100644 index 0000000000..b7b719c3a3 --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/AppRunner.java @@ -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 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 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 getBean(Class type) { + return app.getBean(type); + } + + public ApplicationContext parent() { + return app.getParent(); + } + + public Map getParentBeans(Class 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(); + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/BaseCertTest.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/BaseCertTest.java new file mode 100644 index 0000000000..bf0a555eff --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/BaseCertTest.java @@ -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; + + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientTest.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientTest.java new file mode 100644 index 0000000000..02b00d1b3a --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientTest.java @@ -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 { + + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/KeyAndCert.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/KeyAndCert.java new file mode 100644 index 0000000000..4c58a12531 --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/KeyAndCert.java @@ -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; + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/KeyTool.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/KeyTool.java new file mode 100644 index 0000000000..1ee56ea324 --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/KeyTool.java @@ -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); + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/TlsConfigClientRunner.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/TlsConfigClientRunner.java new file mode 100644 index 0000000000..d5ea3f886f --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/TlsConfigClientRunner.java @@ -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()); + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/TlsConfigServerRunner.java b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/TlsConfigServerRunner.java new file mode 100644 index 0000000000..c585bf8da1 --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/java/org/springframework/cloud/config/client/TlsConfigServerRunner.java @@ -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()); + } + +} diff --git a/spring-cloud-config-client-tls-tests/src/test/resources/test/config/application.properties b/spring-cloud-config-client-tls-tests/src/test/resources/test/config/application.properties new file mode 100644 index 0000000000..2f4082ae3e --- /dev/null +++ b/spring-cloud-config-client-tls-tests/src/test/resources/test/config/application.properties @@ -0,0 +1 @@ +dumb.key=dumb-value diff --git a/spring-cloud-config-client/pom.xml b/spring-cloud-config-client/pom.xml index 6d87819f5f..c779cebc1e 100644 --- a/spring-cloud-config-client/pom.xml +++ b/spring-cloud-config-client/pom.xml @@ -71,6 +71,10 @@ com.fasterxml.jackson.core jackson-databind + + org.apache.httpcomponents + httpclient + org.springframework.boot spring-boot-autoconfigure-processor diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java index 11b215b63a..3286de66f8 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java @@ -22,9 +22,12 @@ 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 void setDiscovery(Discovery discovery) { 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; } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index 2e3d506115..29abbdfc61 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -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.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.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.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 void setRestTemplate(RestTemplate restTemplate) { } 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 headers = new HashMap<>(client.getHeaders()); if (headers.containsKey(AUTHORIZATION)) { @@ -318,6 +325,35 @@ private RestTemplate getSecureRestTemplate(ConfigClientProperties client) { 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);