diff --git a/pom.xml b/pom.xml
index 0c41d1a44..9983be32b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,9 @@
java
1.19.1
1.4.11.1
+
+ 1.64
+ 2.1
@@ -176,6 +179,16 @@
${xstream.version}
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ ${bouncycastle.version}
+
+
+ javax.xml
+ jaxb-impl
+ ${jaxb.version}
+
@@ -204,6 +217,7 @@
spring-cloud-netflix-ribbon
spring-cloud-starter-netflix
spring-cloud-netflix-hystrix
+ spring-cloud-netflix-eureka-client-tls-tests
docs
diff --git a/spring-cloud-netflix-eureka-client-tls-tests/pom.xml b/spring-cloud-netflix-eureka-client-tls-tests/pom.xml
new file mode 100644
index 000000000..a1fe4c08e
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/pom.xml
@@ -0,0 +1,100 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-netflix
+ 2.2.4.BUILD-SNAPSHOT
+ ..
+
+ spring-cloud-netflix-eureka-client-tls-tests
+ jar
+ Spring Cloud Netflix Eureka Client TLS Tests
+ Spring Cloud Netflix Eureka Client TLS Tests
+
+
+
+ org.springframework.cloud
+ spring-cloud-netflix-eureka-client
+
+
+ org.springframework.cloud
+ spring-cloud-netflix-eureka-server
+
+
+
+ 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
+
+
+ javax.xml
+ jaxb-impl
+
+
+
diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/AppRunner.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/AppRunner.java
new file mode 100644
index 000000000..3e48433f9
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/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.netflix.eureka;
+
+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-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java
new file mode 100644
index 000000000..b224ecb0c
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/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.netflix.eureka;
+
+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-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java
new file mode 100644
index 000000000..6923e8ac2
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java
@@ -0,0 +1,99 @@
+/*
+ * 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.netflix.eureka;
+
+import java.io.File;
+import java.util.function.BooleanSupplier;
+
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+
+public class EurekaClientRunner extends AppRunner {
+
+ public EurekaClientRunner(Class> appClass, AppRunner server) {
+ super(appClass);
+
+ property("eureka.client.registerWithEureka", "false");
+ property("eureka.client.fetchRegistry", "true");
+ property("eureka.client.serviceUrl.defaultZone", server.root() + "eureka/");
+ property("eureka.client.refresh.enable", "true");
+ }
+
+ public EurekaClientRunner(Class> appClass, AppRunner server, String service) {
+ this(appClass, server);
+ property("eureka.client.registerWithEureka", "true");
+ property("spring.application.name", service);
+ }
+
+ public void enableTls() {
+ property("eureka.client.tls.enabled", "true");
+ }
+
+ public void disableTls() {
+ property("eureka.client.tls.enabled", "false");
+ }
+
+ public void setKeyStore(File keyStore, String keyStorePassword, String keyPassword) {
+ property("eureka.client.tls.key-store", pathOf(keyStore));
+ property("eureka.client.tls.key-store-password", keyStorePassword);
+ property("eureka.client.tls.key-password", keyPassword);
+ }
+
+ public void setKeyStore(File keyStore) {
+ property("eureka.client.tls.key-store", pathOf(keyStore));
+ }
+
+ public void setTrustStore(File trustStore, String password) {
+ property("eureka.client.tls.trust-store", pathOf(trustStore));
+ property("eureka.client.tls.trust-store-password", password);
+ }
+
+ public void setTrustStore(File trustStore) {
+ property("eureka.client.tls.trust-store", pathOf(trustStore));
+ }
+
+ private String pathOf(File file) {
+ return String.format("file:%s", file.getAbsolutePath());
+ }
+
+ public void waitServiceViaEureka(int seconds) {
+ assertInSeconds(() -> foundServiceViaEureka(), seconds);
+ }
+
+ private void assertInSeconds(BooleanSupplier assertion, int seconds) {
+ long start = System.currentTimeMillis();
+ long limit = 1000L * seconds;
+ long duration = 0;
+
+ do {
+ if (assertion.getAsBoolean()) {
+ return;
+ }
+ duration = System.currentTimeMillis() - start;
+ Thread.yield();
+
+ }
+ while (duration < limit);
+
+ throw new RuntimeException();
+ }
+
+ public boolean foundServiceViaEureka() {
+ DiscoveryClient discovery = getBean(DiscoveryClient.class);
+ return !discovery.getServices().isEmpty();
+ }
+
+}
diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java
new file mode 100644
index 000000000..e2d066bf9
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.netflix.eureka;
+
+import java.io.File;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EurekaClientTest extends BaseCertTest {
+
+ private static EurekaServerRunner server;
+
+ private static EurekaClientRunner service;
+
+ @BeforeClass
+ public static void setupAll() {
+ startEurekaServer();
+ startService();
+ waitForRegistration();
+ }
+
+ @AfterClass
+ public static void tearDownAll() {
+ stopService();
+ stopEurekaServer();
+ }
+
+ private static void startEurekaServer() {
+ server = new EurekaServerRunner(TestEurekaServer.class);
+ server.enableTls();
+ server.setKeyStore(serverCert, KEY_STORE_PASSWORD, "server", KEY_PASSWORD);
+ server.setTrustStore(caCert, KEY_STORE_PASSWORD);
+
+ server.start();
+ }
+
+ private static void stopEurekaServer() {
+ server.stop();
+ }
+
+ private static void startService() {
+ service = new EurekaClientRunner(TestApp.class, server, "testservice");
+ enableTlsClient(service);
+ service.start();
+ }
+
+ private static void stopService() {
+ service.stop();
+ }
+
+ private static void waitForRegistration() {
+ try (EurekaClientRunner client = createEurekaClient()) {
+ enableTlsClient(client);
+ client.start();
+ client.waitServiceViaEureka(60);
+ }
+ }
+
+ private static EurekaClientRunner createEurekaClient() {
+ return new EurekaClientRunner(TestApp.class, server);
+ }
+
+ private static void enableTlsClient(EurekaClientRunner runner) {
+ runner.enableTls();
+ runner.setKeyStore(clientCert, KEY_STORE_PASSWORD, KEY_PASSWORD);
+ runner.setTrustStore(caCert, KEY_STORE_PASSWORD);
+ }
+
+ /**
+ * Already proved this in waitForRegistration(). Keep this Test to express test
+ * purpose explicitly.
+ */
+ @Test
+ public void clientCertCanWork() {
+ }
+
+ @Test
+ public void noCertCannotWork() {
+ try (EurekaClientRunner client = createEurekaClient()) {
+ client.disableTls();
+ client.start();
+ assertThat(client.foundServiceViaEureka()).isFalse();
+ }
+ }
+
+ @Test
+ public void wrongCertCannotWork() {
+ try (EurekaClientRunner client = createEurekaClient()) {
+ enableTlsClient(client);
+ client.setKeyStore(wrongClientCert);
+ client.start();
+ assertThat(client.foundServiceViaEureka()).isFalse();
+ }
+ }
+
+ @Test(expected = BeanCreationException.class)
+ public void wrongPasswordCauseFailure() {
+ EurekaClientRunner client = createEurekaClient();
+ enableTlsClient(client);
+ client.setKeyStore(clientCert, WRONG_PASSWORD, WRONG_PASSWORD);
+ client.start();
+ }
+
+ @Test(expected = BeanCreationException.class)
+ public void nonExistKeyStoreCauseFailure() {
+ EurekaClientRunner client = createEurekaClient();
+ enableTlsClient(client);
+ client.setKeyStore(new File("nonExistFile"));
+ client.start();
+ }
+
+ @Test
+ public void wrongTrustStoreCannotWork() {
+ try (EurekaClientRunner client = createEurekaClient()) {
+ enableTlsClient(client);
+ client.setTrustStore(wrongCaCert);
+ client.start();
+ assertThat(client.foundServiceViaEureka()).isFalse();
+ }
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ public static class TestApp {
+
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ @EnableEurekaServer
+ public static class TestEurekaServer {
+
+ }
+
+}
diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java
new file mode 100644
index 000000000..a5fb15763
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java
@@ -0,0 +1,56 @@
+/*
+ * 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.netflix.eureka;
+
+import java.io.File;
+
+public class EurekaServerRunner extends AppRunner {
+
+ public EurekaServerRunner(Class> appClass) {
+ super(appClass);
+
+ property("eureka.client.registerWithEureka", "false");
+ property("eureka.client.fetchRegistry", "false");
+ property("eureka.server.waitTimeInMsWhenSyncEmpty", "0");
+ property("eureka.client.refresh.enable", "true");
+ }
+
+ 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-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyAndCert.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyAndCert.java
new file mode 100644
index 000000000..192febd07
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/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.netflix.eureka;
+
+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-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyTool.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyTool.java
new file mode 100644
index 000000000..b1e22fea1
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/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.netflix.eureka;
+
+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-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java
index be5d14cc7..88300bcc7 100644
--- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java
+++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java
@@ -16,15 +16,20 @@
package org.springframework.cloud.netflix.eureka.config;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs;
import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs;
import org.springframework.cloud.netflix.eureka.http.WebClientDiscoveryClientOptionalArgs;
@@ -35,8 +40,12 @@ import org.springframework.context.annotation.Configuration;
* @author Daniel Lavoie
*/
@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(TlsProperties.class)
public class DiscoveryClientOptionalArgsConfiguration {
+ @Autowired
+ private TlsProperties tls;
+
protected final Log logger = LogFactory.getLog(getClass());
@Bean
@@ -45,9 +54,12 @@ public class DiscoveryClientOptionalArgsConfiguration {
search = SearchStrategy.CURRENT)
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
matchIfMissing = true, havingValue = "false")
- public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() {
+ public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs()
+ throws GeneralSecurityException, IOException {
logger.info("Eureka HTTP Client uses RestTemplate.");
- return new RestTemplateDiscoveryClientOptionalArgs();
+ RestTemplateDiscoveryClientOptionalArgs result = new RestTemplateDiscoveryClientOptionalArgs();
+ setupTLS(result);
+ return result;
}
@Bean
@@ -60,17 +72,31 @@ public class DiscoveryClientOptionalArgsConfiguration {
search = SearchStrategy.CURRENT)
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
havingValue = "true")
- public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs() {
+ public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs()
+ throws GeneralSecurityException, IOException {
logger.info("Eureka HTTP Client uses WebClient.");
- return new WebClientDiscoveryClientOptionalArgs();
+ WebClientDiscoveryClientOptionalArgs result = new WebClientDiscoveryClientOptionalArgs();
+ setupTLS(result);
+ return result;
}
@Bean
@ConditionalOnClass(name = "com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class,
search = SearchStrategy.CURRENT)
- public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
- return new MutableDiscoveryClientOptionalArgs();
+ public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs()
+ throws GeneralSecurityException, IOException {
+ logger.info("Eureka HTTP Client uses Jersey");
+ MutableDiscoveryClientOptionalArgs result = new MutableDiscoveryClientOptionalArgs();
+ setupTLS(result);
+ return result;
+ }
+
+ private void setupTLS(AbstractDiscoveryClientOptionalArgs> args)
+ throws GeneralSecurityException, IOException {
+ if (tls.isEnabled()) {
+ args.setSSLContext(tls.createSSLContext());
+ }
}
@Configuration
diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java
new file mode 100644
index 000000000..9f8958789
--- /dev/null
+++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2017-2020 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.netflix.eureka.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.UnrecoverableKeyException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+import javax.net.ssl.SSLContext;
+
+import org.apache.http.ssl.SSLContextBuilder;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.io.Resource;
+
+/**
+ * Eureka client TLS properties.
+ */
+@ConfigurationProperties(TlsProperties.PREFIX)
+public class TlsProperties {
+
+ /**
+ * Prefix for Eureka client TLS properties.
+ */
+ public static final String PREFIX = "eureka.client.tls";
+
+ private static final String DEFAULT_STORE_TYPE = "PKCS12";
+
+ private static final Map EXTENSION_STORE_TYPES = extTypes();
+
+ private boolean enabled;
+
+ private Resource keyStore;
+
+ private String keyStoreType;
+
+ private String keyStorePassword = "";
+
+ private String keyPassword = "";
+
+ private Resource trustStore;
+
+ private String trustStoreType;
+
+ private String trustStorePassword = "";
+
+ private static Map extTypes() {
+ Map result = new HashMap<>();
+
+ result.put("p12", "PKCS12");
+ result.put("pfx", "PKCS12");
+ result.put("jks", "JKS");
+
+ return Collections.unmodifiableMap(result);
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public Resource getKeyStore() {
+ return keyStore;
+ }
+
+ public void setKeyStore(Resource keyStore) {
+ this.keyStore = keyStore;
+ }
+
+ public String getKeyStoreType() {
+ return keyStoreType;
+ }
+
+ public void setKeyStoreType(String keyStoreType) {
+ this.keyStoreType = keyStoreType;
+ }
+
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public void setKeyStorePassword(String keyStorePassword) {
+ this.keyStorePassword = keyStorePassword;
+ }
+
+ public char[] keyStorePassword() {
+ return keyStorePassword.toCharArray();
+ }
+
+ public String getKeyPassword() {
+ return keyPassword;
+ }
+
+ public void setKeyPassword(String keyPassword) {
+ this.keyPassword = keyPassword;
+ }
+
+ public char[] keyPassword() {
+ return keyPassword.toCharArray();
+ }
+
+ public Resource getTrustStore() {
+ return trustStore;
+ }
+
+ public void setTrustStore(Resource trustStore) {
+ this.trustStore = trustStore;
+ }
+
+ public String getTrustStoreType() {
+ return trustStoreType;
+ }
+
+ public void setTrustStoreType(String trustStoreType) {
+ this.trustStoreType = trustStoreType;
+ }
+
+ public String getTrustStorePassword() {
+ return trustStorePassword;
+ }
+
+ public void setTrustStorePassword(String trustStorePassword) {
+ this.trustStorePassword = trustStorePassword;
+ }
+
+ public char[] trustStorePassword() {
+ return trustStorePassword.toCharArray();
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ if (keyStore != null && keyStoreType == null) {
+ keyStoreType = storeTypeOf(keyStore);
+ }
+ if (trustStore != null && trustStoreType == null) {
+ trustStoreType = storeTypeOf(trustStore);
+ }
+ }
+
+ private String storeTypeOf(Resource resource) {
+ String extension = fileExtensionOf(resource);
+ String type = EXTENSION_STORE_TYPES.get(extension);
+
+ return (type == null) ? DEFAULT_STORE_TYPE : type;
+ }
+
+ private String fileExtensionOf(Resource resource) {
+ String name = resource.getFilename();
+ int index = name.lastIndexOf('.');
+
+ return index < 0 ? "" : name.substring(index + 1).toLowerCase();
+ }
+
+ public SSLContext createSSLContext() throws GeneralSecurityException, IOException {
+ SSLContextBuilder builder = new SSLContextBuilder();
+ char[] keyPassword = keyPassword();
+ KeyStore keyStore = createKeyStore();
+
+ try {
+ builder.loadKeyMaterial(keyStore, keyPassword);
+ }
+ catch (UnrecoverableKeyException e) {
+ if (keyPassword.length == 0) {
+ // Retry if empty password, see
+ // https://rt.openssl.org/Ticket/Display.html?id=1497&user=guest&pass=guest
+ builder.loadKeyMaterial(keyStore, new char[] { '\0' });
+ }
+ else {
+ throw e;
+ }
+ }
+
+ KeyStore trust = createTrustStore();
+ if (trust != null) {
+ builder.loadTrustMaterial(trust, null);
+ }
+
+ return builder.build();
+ }
+
+ private KeyStore createKeyStore() throws GeneralSecurityException, IOException {
+ if (keyStore == null) {
+ throw new KeyStoreException("Keystore not specified.");
+ }
+ if (!keyStore.exists()) {
+ throw new KeyStoreException("Keystore not exists: " + keyStore);
+ }
+
+ KeyStore result = KeyStore.getInstance(keyStoreType);
+ char[] keyStorePassword = keyStorePassword();
+
+ try {
+ loadKeyStore(result, keyStore, keyStorePassword);
+ }
+ catch (IOException e) {
+ // Retry if empty password, see
+ // https://rt.openssl.org/Ticket/Display.html?id=1497&user=guest&pass=guest
+ if (keyStorePassword.length == 0) {
+ loadKeyStore(result, keyStore, new char[] { '\0' });
+ }
+ else {
+ throw e;
+ }
+ }
+
+ return result;
+ }
+
+ private static void loadKeyStore(KeyStore keyStore, Resource keyStoreResource,
+ char[] keyStorePassword) throws IOException, GeneralSecurityException {
+ try (InputStream inputStream = keyStoreResource.getInputStream()) {
+ keyStore.load(inputStream, keyStorePassword);
+ }
+ }
+
+ private KeyStore createTrustStore() throws GeneralSecurityException, IOException {
+ if (trustStore == null) {
+ return null;
+ }
+ if (!trustStore.exists()) {
+ throw new KeyStoreException("KeyStore not exists: " + trustStore);
+ }
+
+ KeyStore result = KeyStore.getInstance(trustStoreType);
+ try (InputStream input = trustStore.getInputStream()) {
+ result.load(input, trustStorePassword());
+ }
+ return result;
+ }
+
+}