diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 2ab6834af..3f3389b7b 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -905,6 +905,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. 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 cc0e2f68d..c32a3e326 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; @@ -45,6 +51,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; @@ -121,6 +128,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; @@ -162,10 +170,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(trustedX509Certificates); + }); + } + else if (ssl.isUseInsecureTrustManager()) { opts.sslSupport(sslContextBuilder -> { - sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + 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 da365d5f0..ca9159ea9 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 000000000..c936eb75c --- /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 000000000..248e6b08b --- /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 000000000..1acba71d0 --- /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 000000000..53de3a9d9 Binary files /dev/null and b/spring-cloud-gateway-core/src/test/resources/scg-keystore.p12 differ