From ffbe5572f48532462492bba425f7b64f03dc9dd5 Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Wed, 18 Jul 2018 15:23:28 -0400 Subject: [PATCH 1/2] Create a trust manager backed by known trusted X509 certificates --- .../config/GatewayAutoConfiguration.java | 21 +++- .../gateway/config/HttpClientProperties.java | 50 +++++++++- .../cloud/gateway/test/ssl/SSLTests.java | 93 ++++++++++++++++++ .../src/test/resources/application-ssl.yml | 38 +++++++ .../src/test/resources/scg-cert.pem | 27 +++++ .../src/test/resources/scg-keystore.p12 | Bin 0 -> 3422 bytes 6 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java create mode 100644 spring-cloud-gateway-core/src/test/resources/application-ssl.yml create mode 100644 spring-cloud-gateway-core/src/test/resources/scg-cert.pem create mode 100644 spring-cloud-gateway-core/src/test/resources/scg-keystore.p12 diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 1317550a..f1faa779 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -17,6 +17,12 @@ package org.springframework.cloud.gateway.config; +import java.io.IOException; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -43,6 +49,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.web.server.WebServerException; import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint; import org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter; import org.springframework.cloud.gateway.filter.ForwardPathFilter; @@ -118,6 +125,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.DispatcherHandler; @@ -159,10 +167,17 @@ public class GatewayAutoConfiguration { // configure ssl HttpClientProperties.Ssl ssl = properties.getSsl(); - - if (ssl.isUseInsecureTrustManager()) { + X509Certificate[] trustedX509Certificates = ssl + .getTrustedX509CertificatesForTrustManager(); + if (trustedX509Certificates.length > 0) { opts.sslSupport(sslContextBuilder -> { - sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + sslContextBuilder.trustManager(trustedX509Certificates); + }); + } + else if (ssl.isUseInsecureTrustManager()) { + opts.sslSupport(sslContextBuilder -> { + sslContextBuilder + .trustManager(InsecureTrustManagerFactory.INSTANCE); }); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java index da365d5f..ca9159ea 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java @@ -18,9 +18,19 @@ package org.springframework.cloud.gateway.config; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.server.WebServerException; +import org.springframework.util.ResourceUtils; + import reactor.ipc.netty.resources.PoolResources; +import java.io.IOException; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; /** * Configuration properties for the Netty {@link reactor.ipc.netty.http.client.HttpClient} @@ -210,6 +220,41 @@ public class HttpClientProperties { public class Ssl { /** Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production. */ private boolean useInsecureTrustManager = false; + + private List trustedX509Certificates = new ArrayList<>(); + + public List getTrustedX509Certificates() { + return trustedX509Certificates; + } + + public X509Certificate[] getTrustedX509CertificatesForTrustManager() { + try { + CertificateFactory certificateFactory = CertificateFactory + .getInstance("X.509"); + ArrayList certs = new ArrayList<>(); + for (String trustedCert : ssl.getTrustedX509Certificates()) { + try { + URL url = ResourceUtils.getURL(trustedCert); + X509Certificate cert = (X509Certificate) certificateFactory + .generateCertificate(url.openStream()); + certs.add(cert); + } + catch (IOException e) { + throw new WebServerException( + "Could not load certificate '" + trustedCert + "'", e); + } + } + return certs.toArray(new X509Certificate[certs.size()]); + } + catch (CertificateException e1) { + throw new WebServerException("Could not load CertificateFactory X.509", + e1); + } + } + + public void setTrustedX509Certificates(List trustedX509) { + this.trustedX509Certificates = trustedX509; + } //TODO: support configuration of other trust manager factories @@ -223,9 +268,8 @@ public class HttpClientProperties { @Override public String toString() { - return "Ssl{" + - "useInsecureTrustManager=" + useInsecureTrustManager + - '}'; + return "Ssl {useInsecureTrustManager=" + useInsecureTrustManager + + ", trustedX509Certificates=" + trustedX509Certificates + "}"; } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java new file mode 100644 index 00000000..c936eb75 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2017 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 + * + * http://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.gateway.test.ssl; + +import static org.junit.Assert.assertTrue; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import javax.net.ssl.SSLException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ActiveProfiles("ssl") +public class SSLTests extends BaseWebClientTests { + + @Before + public void setup() { + try { + SslContext sslContext = SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + ClientHttpConnector httpConnector = new ReactorClientHttpConnector( + opt -> opt.sslContext(sslContext)); + baseUri = "https://localhost:" + port; + this.webClient = WebClient.builder().clientConnector(httpConnector) + .baseUrl(baseUri).build(); + } + catch (SSLException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testSslTrust() { + ClientResponse clientResponse = webClient.get().uri("/ssltrust") + .exchange().block(); + HttpStatus statusCode = clientResponse.statusCode(); + assertTrue(statusCode.is2xxSuccessful()); + + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + @RestController + public static class TestConfig { + + @RequestMapping("/httpbin/ssltrust") + public ResponseEntity nocontenttype() { + return ResponseEntity.status(204).build(); + } + + } + +} diff --git a/spring-cloud-gateway-core/src/test/resources/application-ssl.yml b/spring-cloud-gateway-core/src/test/resources/application-ssl.yml new file mode 100644 index 00000000..248e6b08 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/resources/application-ssl.yml @@ -0,0 +1,38 @@ +test: + hostport: httpbin.org:80 + uri: lb://testservice + +server: + ssl: + enabled: true + key-alias: scg + key-store-password: scg1234 + key-store: classpath:scg-keystore.p12 + key-store-type: PKCS12 + +spring: + cloud: + gateway: + httpclient: + ssl: + trustedX509Certificates: + - src/test/resources/scg-cert.pem + default-filters: + - PrefixPath=/httpbin + routes: + - id: default_path_to_httpbin + uri: ${test.uri} + order: 10000 + predicates: + - name: Path + args: + pattern: /** + +logging: + level: + org.springframework.cloud.gateway: TRACE + org.springframework.http.server.reactive: DEBUG + org.springframework.web.reactive: DEBUG + reactor.ipc.netty: DEBUG + redisratelimiter: DEBUG + diff --git a/spring-cloud-gateway-core/src/test/resources/scg-cert.pem b/spring-cloud-gateway-core/src/test/resources/scg-cert.pem new file mode 100644 index 00000000..1acba71d --- /dev/null +++ b/spring-cloud-gateway-core/src/test/resources/scg-cert.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEjTCCAvWgAwIBAgIEIZEBKTANBgkqhkiG9w0BAQwFADBsMRAwDgYDVQQGEwdV +bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD +VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du +MB4XDTE4MDcxODEyNTY0OFoXDTQ1MTIwMzEyNTY0OFowbDEQMA4GA1UEBhMHVW5r +bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE +ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC +AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJhdOwHjozNaUdocb+p/Dn6E +UUXzkNGwmUZwRSQKnEK9IV/PTwrzK+fPJS8vpUzEpj37QIGBTSNuebQ3w5G4cAu6 +xFEk4e56C823Vio5Hae/+fos7goaR+ihKMy2lGPwFGNECqGmUZItE1+4pdjYyia7 +FTWluAVuTgue3VODl9ziDw+2Zrt6Axtnb5heQXP8ElEZldoePpUmbLWa03Xvfatb +4ZKkoCLOKhcgM5F2tyR+VlZnqm2dhvO+J778MsU4ToVAUGrVIkeQi0BHTi5Rzy2c +2kDLHHuJTYqN65sl5LyKDu003KXioelZVUeH3Hgtrj0Tt96oYSixvHSGwDmcXpux +4sbzTS7x/KeQAjLIFj/3rxQP0TSuTCe/XrqiCpFflLntTI9En9CWto6SXFRigval +zCITgImQthSfFMxFDqNtNQo5hTAu8VY2/FurMbkukQ5l2+vU4dKBccNxChj5m7p3 +8C4G7Rh08AEYaNYcNZlaz++c37rW7P6vWX8m6C21CQIDAQABozcwNTAUBgNVHREE +DTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFDOSE31Vw2QdyVviz+H5+tY5HRR4MA0G +CSqGSIb3DQEBDAUAA4IBgQAJljlIubbQvw/2UVymsyF939XKus/7muiJLtQt0J4J +sSuGiSQmkyJajBRj9+qsLLTtdL6F2+BFJtP8S/zZixrEzktuRZ3b40MLtyI4hVDt +fmgj3UMmphVgbmDv71WvRmUXFfBX5Zka7zRW+lFvO5dLytegKi3Vc8zOFRUcvY9w +uBEOipAkUqH6y+lI/lJ72MrXpkxbkAq2fYffZK4e6KNpKG7pP0txEkwNvosKSAjA +yL+Ye5bK6sYbscwsqvXw9nWwjuFpj/0zvD/tM1jMtUtV1+9HKC3vsfAaL9QnwnS6 +qhSaos8j7fw9SYdAoO+yth2w1ETxKXrzcF+LEkhzj2R0zygycLZvWj51lIWDDOvy +m3IUFTY8fX7CBgLIGnHAcdWK6a+FkWIb8WybWkv2n2SxM4AnzXb5epk/K2Uo30hY +mr9wrywvo92xUjXEOmQpAbFx0hcVjYOtvpHdUeZTGLLQ7sWJGQFalRT+GsTy+Lph +10719GizKXQW2Mx/8XOr1Ow= +-----END CERTIFICATE----- diff --git a/spring-cloud-gateway-core/src/test/resources/scg-keystore.p12 b/spring-cloud-gateway-core/src/test/resources/scg-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..53de3a9d91f71b2aba74ab8d04e7309c91f6cc5a GIT binary patch literal 3422 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 literal 0 HcmV?d00001 From 474b50a633440a9b999bd564d6a364187a9dcfff Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Wed, 18 Jul 2018 17:20:10 -0400 Subject: [PATCH 2/2] Document Gateway TLS trust manager options --- .../main/asciidoc/spring-cloud-gateway.adoc | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 2d02210b..3c42e980 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -894,6 +894,50 @@ or check if an exchange has already been routed. * `ServerWebExchangeUtils.isAlreadyRouted` takes a `ServerWebExchange` object and checks if it has been "routed" * `ServerWebExchangeUtils.setAlreadyRouted` takes a `ServerWebExchange` object and marks it as "routed" +== TLS / SSL +The Gateway can listen for requests on https by following the usual Spring server configuration. Example: + +.application.yml +[source,yaml] +---- +server: + ssl: + enabled: true + key-alias: scg + key-store-password: scg1234 + key-store: classpath:scg-keystore.p12 + key-store-type: PKCS12 +---- + +Gateway routes can be routed to both http and https backends. If routing to a https backend then the Gateway can be configured to trust all downstream certificates with the following configuration: + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + httpclient: + ssl: + useInsecureTrustManager: true +---- + +Using an insecure trust manager is not suitable for production. For a production deployment the Gateway can be configured with a set of known certificates that it can trust with the follwing configuration: + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + httpclient: + ssl: + trustedX509Certificates: + - cert1.pem + - cert2.pem +---- + + == Configuration Configuration for Spring Cloud Gateway is driven by a collection of `RouteDefinitionLocator`s.