diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index 03cbe97c87..f3e99bc454 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -10,8 +10,6 @@ spring: gateway: mvc.enabled: false enabled: false - mvc: - throw-exception-if-no-handler-found: true output: ansi: enabled: detect diff --git a/api-catalog-services/src/test/resources/application.yml b/api-catalog-services/src/test/resources/application.yml index cf62bca930..47439542ba 100644 --- a/api-catalog-services/src/test/resources/application.yml +++ b/api-catalog-services/src/test/resources/application.yml @@ -10,8 +10,6 @@ spring: client: hostname: ${apiml.service.hostname} ipAddress: ${apiml.service.ipAddress} - mvc: - throw-exception-if-no-handler-found: true output: ansi: enabled: detect diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java index db2131035c..3659df3ae7 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/verify/CertificateValidator.java @@ -57,6 +57,8 @@ public CertificateValidator(TrustedCertificatesProvider trustedCertificatesProvi * @return true if all given certificates are known false otherwise */ public boolean hasGatewayChain(X509Certificate[] certs) { + if (certs == null || certs.length == 0) return false; + if ((proxyCertificatesEndpoints == null) || (proxyCertificatesEndpoints.length == 0)) { log.debug("No endpoint configured to retrieve trusted certificates. Provide URL via apiml.security.x509.certificatesUrls"); return false; diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index 51f75867c5..c72efff9c6 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -16,15 +16,17 @@ eureka: spring: cloud: gateway: - route-refresh-listener.enabled: false - x-forwarded: - prefix-append: false - prefix-enabled: true - filter: - secure-headers: - disable: content-security-policy,permitted-cross-domain-policies,download-options - referrer-policy: strict-origin-when-cross-origin - frame-options: sameorigin + server: + webflux: + route-refresh-listener.enabled: false + x-forwarded: + prefix-append: false + prefix-enabled: true + filter: + secure-headers: + disable: content-security-policy,permitted-cross-domain-policies,download-options + referrer-policy: strict-origin-when-cross-origin + frame-options: sameorigin application: name: gateway security: @@ -187,16 +189,14 @@ logging: management: endpoint: gateway: - enabled: false + access: none health: showDetails: always - shutdown: - enabled: true endpoints: web: base-path: /application exposure: - include: health,info,gateway,shutdown + include: health,info,gateway --- spring.config.activate.on-profile: debug diff --git a/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java b/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java index e36e7fccac..ebac7387d3 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java +++ b/common-service-core/src/main/java/org/zowe/apiml/security/SecurityUtils.java @@ -24,6 +24,7 @@ import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Base64; import java.util.Enumeration; @@ -126,6 +127,10 @@ public static Set loadCertificateChainBase64(HttpsConfig config) throws return out; } + public String base64EncodePublicKey(X509Certificate cert) { + return Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded()); + } + /** * Loads public key from keystore or key ring, if keystore URL has proper format {@link #KEYRING_PATTERN} * diff --git a/config/docker/apiml.yml b/config/docker/apiml.yml index 5c5c93a4cf..f206a85f35 100644 --- a/config/docker/apiml.yml +++ b/config/docker/apiml.yml @@ -22,6 +22,16 @@ eureka: initialInstanceInfoReplicationIntervalSeconds: 1 registryFetchIntervalSeconds: 1 +management: + endpoints: + migrate-legacy-ids: true + web: + exposure: + include: "*" + endpoint: + shutdown: + access: unrestricted + server: address: 0.0.0.0 max-http-header-size: 40000 diff --git a/config/docker/discovery-service.yml b/config/docker/discovery-service.yml index d4bc99ddf1..9055df12a1 100644 --- a/config/docker/discovery-service.yml +++ b/config/docker/discovery-service.yml @@ -20,6 +20,17 @@ eureka: client: initialInstanceInfoReplicationIntervalSeconds: 1 registryFetchIntervalSeconds: 1 + +management: + endpoints: + migrate-legacy-ids: true + web: + exposure: + include: "*" + endpoint: + shutdown: + access: unrestricted + --- spring.config.activate.on-profile: https diff --git a/config/docker/gateway-service.yml b/config/docker/gateway-service.yml index c503f5aba6..2a0768a12f 100644 --- a/config/docker/gateway-service.yml +++ b/config/docker/gateway-service.yml @@ -14,6 +14,16 @@ spring: ansi: enabled: always +management: + endpoints: + migrate-legacy-ids: true + web: + exposure: + include: "*" + endpoint: + shutdown: + access: unrestricted + server: address: 0.0.0.0 max-http-header-size: 40000 diff --git a/config/docker/zaas-service.yml b/config/docker/zaas-service.yml index 434a2c1f75..157e7ac9a0 100644 --- a/config/docker/zaas-service.yml +++ b/config/docker/zaas-service.yml @@ -37,7 +37,15 @@ spring: output: ansi: enabled: always - +management: + endpoints: + migrate-legacy-ids: true + web: + exposure: + include: "*" + endpoint: + shutdown: + access: unrestricted server: address: 0.0.0.0 diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index fcc2d4bbe0..0af3d08f8c 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -21,8 +21,6 @@ spring: enabled: false # Should be removed when upgrade to Spring Cloud 3.x application: name: ${apiml.service.serviceId} - mvc: - throw-exception-if-no-handler-found: true output: ansi: enabled: detect @@ -34,7 +32,8 @@ spring: graphql: graphiql: enabled: true - path: "/api/v3/graphql" + http: + path: "/api/v3/graphql" eureka: client: @@ -183,7 +182,7 @@ management: enabled: true endpoint: shutdown: - enabled: true + access: unrestricted --- spring.config.activate.on-profile: diag @@ -197,7 +196,7 @@ management: include: "*" endpoint: shutdown: - enabled: true + access: unrestricted --- spring.config.activate.on-profile: debug diff --git a/discoverable-client/src/test/resources/application.yml b/discoverable-client/src/test/resources/application.yml index abf9043dfb..6b45c227ec 100644 --- a/discoverable-client/src/test/resources/application.yml +++ b/discoverable-client/src/test/resources/application.yml @@ -18,7 +18,6 @@ spring: application: name: ${apiml.service.serviceId} mvc: - throw-exception-if-no-handler-found: true pathmatch: matchingStrategy: ant-path-matcher # used to resolve spring fox path matching issue output: diff --git a/discovery-service/src/main/resources/application.yml b/discovery-service/src/main/resources/application.yml index a9962f7946..cd54547221 100644 --- a/discovery-service/src/main/resources/application.yml +++ b/discovery-service/src/main/resources/application.yml @@ -98,13 +98,11 @@ management: web: base-path: /application exposure: - include: health,info,shutdown,hystrixstream + include: health,info,hystrixstream health: defaults: enabled: false endpoint: - shutdown: - enabled: true health: show-details: always @@ -155,10 +153,7 @@ management: web: base-path: /application exposure: - include: health,info,loggers,shutdown - endpoint: - shutdown: - enabled: true + include: health,info,loggers logging: level: diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 105397f4f9..53cfceac74 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -321,6 +321,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \ -Dapiml.security.x509.certificatesUrls=${ZWE_configs_apiml_security_x509_certificatesUrls:-${ZWE_configs_apiml_security_x509_certificatesUrl:-}} \ -Dapiml.security.x509.enabled=${ZWE_configs_apiml_security_x509_enabled:-false} \ -Dapiml.security.x509.registry.allowedUsers=${ZWE_configs_apiml_security_x509_registry_allowedUsers:-} \ + -Dapiml.security.forwardHeader.trustedProxies=${ZWE_configs_apiml_security_forwardHeader_trustedProxies:-${ZWE_components_cloudGateway_apiml_security_forwardHeader_trustedProxies:-}} \ -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-true} \ -Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \ -Dapiml.service.corsEnabled=${ZWE_configs_apiml_service_corsEnabled:-false} \ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java index 9012d06b14..dc44ad81a9 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/ConnectionsConfig.java @@ -20,6 +20,7 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.resolver.DefaultAddressResolverGroup; import jakarta.annotation.PostConstruct; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; @@ -149,6 +150,8 @@ public class ConnectionsConfig { private boolean corsEnabled; private final ApplicationContext context; private static final ApimlLogger apimlLog = ApimlLogger.of(ConnectionsConfig.class, YamlMessageServiceInstance.getInstance()); + private HttpsConfig httpsConfig; + @Getter private HttpsFactory httpsFactory; public ConnectionsConfig(ApplicationContext context) { @@ -171,22 +174,23 @@ public void updateConfigParameters() { serverProperties.getSsl().setTrustStore(trustStorePath); if (trustStorePassword == null) trustStorePassword = KEYRING_PASSWORD; } - httpsFactory = factory(); - } - public HttpsFactory factory() { - HttpsConfig config = HttpsConfig.builder() + httpsConfig = HttpsConfig.builder() .protocol(protocol) .verifySslCertificatesOfServices(verifySslCertificatesOfServices) .nonStrictVerifySslCertificatesOfServices(nonStrictVerifySslCertificatesOfServices) .trustStorePassword(trustStorePassword).trustStoreRequired(trustStoreRequired) - .idleConnTimeoutSeconds(idleConnTimeoutSeconds).requestConnectionTimeout(requestTimeout) .trustStore(trustStorePath).trustStoreType(trustStoreType) .keyAlias(keyAlias).keyStore(keyStorePath).keyPassword(keyPassword) .keyStorePassword(keyStorePassword).keyStoreType(keyStoreType).build(); - log.info("Using HTTPS configuration: {}", config.toString()); + log.info("Using HTTPS configuration: {}", httpsConfig.toString()); + + httpsFactory = new HttpsFactory(httpsConfig); + } - return new HttpsFactory(config); + @Bean + public HttpsConfig httpsConfig() { + return httpsConfig; } /** @@ -270,7 +274,7 @@ SslContext getSslContext(boolean setKeystore) { } catch (Exception e) { apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, - HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, factory().getConfig()); + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, httpsFactory.getConfig()); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index e4ad5da74d..0b3117f33b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -16,7 +16,9 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -58,6 +60,7 @@ import org.springframework.web.server.WebFilter; import org.zowe.apiml.gateway.config.oidc.ClientConfiguration; import org.zowe.apiml.gateway.controllers.GatewayExceptionHandler; +import org.zowe.apiml.gateway.filters.X509awareXForwardedHeadersFilter; import org.zowe.apiml.gateway.filters.security.AuthExceptionHandlerReactive; import org.zowe.apiml.gateway.filters.security.BasicAuthFilter; import org.zowe.apiml.gateway.filters.security.TokenAuthFilter; @@ -65,13 +68,17 @@ import org.zowe.apiml.gateway.service.TokenProvider; import org.zowe.apiml.gateway.x509.X509Util; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.config.SafSecurityConfigurationProperties; import reactor.core.publisher.Mono; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -514,4 +521,13 @@ public StrictServerWebExchangeFirewall httpFirewall() { return firewall; } + @Bean + @Primary + @ConditionalOnProperty(name = "spring.cloud.gateway.x-forwarded.enabled", matchIfMissing = true) + public XForwardedHeadersFilter xForwardedHeadersFilter( + HttpsConfig httpsConfig, + @Value("${apiml.security.forwardHeader.trustedProxies:#{null}}") String trustedProxies) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + return new X509awareXForwardedHeadersFilter(httpsConfig, trustedProxies); + } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509awareXForwardedHeadersFilter.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509awareXForwardedHeadersFilter.java new file mode 100644 index 0000000000..cf97b2d2d8 --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/X509awareXForwardedHeadersFilter.java @@ -0,0 +1,137 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.filters; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.http.server.reactive.SslInfo; +import org.springframework.web.server.ServerWebExchange; +import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.security.SecurityUtils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * The aim of the class is to ensure that the content of X-Forwarded-* or Forwarded headers contains only trusted data. + * Forged values of these headers can pose a security risk as identified by CVE-2025-41235. + * Trusted proxies can be defined via config property `apiml.security.forwardHeader.trustedProxies` that is equivalent of + * the Spring's original one `spring.cloud.gateway.mvc.trustedProxies`. The benefit of this bean is that it is not + * necessary to validate headers if the request is signed by trusted Gateway certificate. It solves, for example, + * the case when multiple APIML Gateways routes each other. The http context cannot be compromised when the request + * is signed by a trusted certificate, so the content of headers is considered valid, and it is not necessary to verify + * the host against the list. Otherwise, if the request is not signed, the client address should be validated against configuration. + * + * The implementation supports empty configuration value. The empty value means when the request is signed, the + * content of headers is accepted otherwise the headers are considered vulnerable and are removed from the request. + * + * Signed / defined pattern | empty list of trusted proxies | a trusted proxy is defined + * -------------------------+-------------------------------+--------------------------- + * untrusted signature/no | headers removed | check against the list + * -------------------------+-------------------------------+--------------------------- + * trusted signature | forward headers | forward headers + * + */ + +@Slf4j +public class X509awareXForwardedHeadersFilter extends XForwardedHeadersFilter { + + // Generic all-in-one Forwarded header not handled by the default spring filter + public static final String FORWARDED_HEADER = "Forwarded"; + + final Set certificateChainBase64; + final Predicate isTrusted; + final String trustedProxies; + + /* + * + * @param httpsConfig gateway certificate configuration + * @param trustedProxies configuration value of a pattern on how validate proxy + * + */ + public X509awareXForwardedHeadersFilter(HttpsConfig httpsConfig, String trustedProxiesPattern) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + certificateChainBase64 = SecurityUtils.loadCertificateChainBase64(httpsConfig); + trustedProxies = trustedProxiesPattern; + if (StringUtils.isEmpty(trustedProxies)) { + isTrusted = host -> false; + } else { + Pattern pattern = Pattern.compile(trustedProxies); + isTrusted = host -> host != null && pattern.matcher(host).matches(); + } + } + + @Override + public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { + boolean trustedSourceByX509 = Optional.ofNullable(exchange.getRequest().getSslInfo()) + .map(SslInfo::getPeerCertificates) + .filter(certs -> certs.length > 0) + .map(certs -> Arrays.stream(certs) + .map(SecurityUtils::base64EncodePublicKey) + .allMatch(certificateChainBase64::contains) + ) + .orElse(false); + + if (!trustedSourceByX509) { + ServerHttpRequest request = exchange.getRequest(); + InetSocketAddress remoteAddress = request.getRemoteAddress(); + if (remoteAddress == null) { + log.trace("Remote address is null and cannot be evaluated for trusted proxy."); + return super.filter(removeXForwardHttpHeaders(input), exchange); + } + if (!isTrusted.test(remoteAddress.getHostString())) { + //Mask the address if it is not trusted so it cannot be used to build the forward headers + ServerWebExchange sanitizedExchange = exchange.mutate().request( + new ServerHttpRequestDecorator(request) { + @Override + public InetSocketAddress getRemoteAddress() { + return null; + } + } + ).build(); + log.trace("Remote address not trusted. Trusted proxies pattern: {}, remote address: {}", trustedProxies, remoteAddress); + return super.filter(removeXForwardHttpHeaders(input), sanitizedExchange); + } + } + return super.filter(input, exchange); + } + + private HttpHeaders removeXForwardHttpHeaders(HttpHeaders input) { + HttpHeaders h = new HttpHeaders(); + input.forEach( (header, values) -> { + if (!isXForwardedHeader(header)) { + h.put(header, values); + } + }); + return h; + } + + private boolean isXForwardedHeader(String header) { + return header.equalsIgnoreCase(X_FORWARDED_FOR_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_HOST_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_PORT_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_PROTO_HEADER) || + header.equalsIgnoreCase(X_FORWARDED_PREFIX_HEADER) || + header.equalsIgnoreCase(FORWARDED_HEADER); + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java index e47dc495a1..e59a61e0bf 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/CertificateChainService.java @@ -14,7 +14,6 @@ import lombok.extern.slf4j.Slf4j; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.springframework.stereotype.Service; -import org.zowe.apiml.gateway.config.ConnectionsConfig; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.security.HttpsConfig; @@ -36,7 +35,7 @@ public class CertificateChainService { private static final ApimlLogger apimlLog = ApimlLogger.of(CertificateChainService.class, YamlMessageServiceInstance.getInstance()); Certificate[] certificates; - private final ConnectionsConfig connectionsConfig; + private final HttpsConfig httpsConfig; public String getCertificatesInPEMFormat() { StringWriter stringWriter = new StringWriter(); @@ -56,13 +55,12 @@ public String getCertificatesInPEMFormat() { @PostConstruct void loadCertChain() { - HttpsConfig config = connectionsConfig.factory().getConfig(); try { - certificates = SecurityUtils.loadCertificateChain(config); + certificates = SecurityUtils.loadCertificateChain(httpsConfig); } catch (Exception e) { apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), - e, HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, config); + e, HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, httpsConfig); } } } diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 3297f46add..5f8a70e280 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -53,14 +53,16 @@ eureka: spring: cloud: gateway: - x-forwarded: - prefix-append: false - prefix-enabled: true - filter: - secure-headers: - disable: content-security-policy,permitted-cross-domain-policies,download-options - referrer-policy: strict-origin-when-cross-origin - frame-options: sameorigin + server: + webflux: + x-forwarded: + prefix-append: false + prefix-enabled: true + filter: + secure-headers: + disable: content-security-policy,permitted-cross-domain-policies,download-options + referrer-policy: strict-origin-when-cross-origin + frame-options: sameorigin application: name: gateway security: @@ -154,11 +156,9 @@ logging: management: endpoint: gateway: - enabled: false + access: none health: showDetails: always - shutdown: - enabled: true health: diskspace: enabled: false @@ -166,7 +166,7 @@ management: web: base-path: /application exposure: - include: health,info,gateway,shutdown + include: health,info,gateway --- spring.config.activate.on-profile: debug diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java index c890dae318..4b02aa6e79 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ForwardedProxyHeadersTest.java @@ -31,9 +31,10 @@ @ActiveProfiles("ForwardedProxyHeadersTest") @TestPropertySource(properties = { "apiml.service.corsEnabled=false", - "spring.cloud.gateway.x-forwarded.for-append=false", - "spring.cloud.gateway.x-forwarded.prefix-enabled=true", - "spring.cloud.gateway.x-forwarded.prefix-append=true" + "spring.cloud.gateway.server.webflux.x-forwarded.for-append=false", + "spring.cloud.gateway.server.webflux.x-forwarded.prefix-enabled=true", + "spring.cloud.gateway.server.webflux.x-forwarded.prefix-append=true", + "spring.cloud.gateway.server.webflux.trusted-proxies=.*" }) class ForwardedProxyHeadersTest extends AcceptanceTestWithMockServices { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java index 444929e3fb..f00d9cf2cc 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/AcceptanceTestWithMockServices.java @@ -10,20 +10,59 @@ package org.zowe.apiml.gateway.acceptance.common; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.TestInstance; +import io.restassured.config.RestAssuredConfig; +import io.restassured.config.SSLConfig; +import lombok.SneakyThrows; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.ssl.PrivateKeyDetails; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.TrustStrategy; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.ResourceUtils; import org.zowe.apiml.gateway.ApplicationRegistry; import org.zowe.apiml.gateway.MockService; +import javax.net.ssl.SSLContext; +import java.net.Socket; +import java.security.cert.X509Certificate; +import java.util.Map; + @AcceptanceTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class AcceptanceTestWithMockServices extends AcceptanceTestWithBasePath { + public RestAssuredConfig apimlCert; + public RestAssuredConfig clientCert; + + @Value("${test.proxyAddress}") + public String proxyAddress; + + @Value("${server.ssl.keyStore}") + private String apimlKeyStorePath; + + @Value("${server.ssl.keyStorePassword}") + private char[] apimlKeyStorePassword; + + @Value("${server.ssl.keyPassword:}") + private char[] apimlKeyPassword; + + @Value("${server.ssl.clientKeyStore:}") + private String clientKeyStorePath; + + @Value("${server.ssl.clientKeyStorePassword}") + private char[] clientKeyStorePassword; + + @Value("${server.ssl.keyPassword}") + private char[] clientKeyPassword; + + @Value("${server.ssl.clientCN}") + private String clientCN; + @Autowired private ApplicationEventPublisher applicationEventPublisher; @@ -40,6 +79,27 @@ void checkAssertionErrorsOnMockServices() { MockService.checkAssertionErrors(); } + @BeforeAll + @SneakyThrows + void init() { + TrustStrategy trustStrategy = (X509Certificate[] chain, String authType) -> true; + X509HostnameVerifier hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; + + SSLContext apimlSSLContext = SSLContextBuilder.create() + .loadKeyMaterial(ResourceUtils.getFile(apimlKeyStorePath), apimlKeyStorePassword, apimlKeyPassword) + .loadTrustMaterial(null, trustStrategy).build(); + apimlCert = RestAssuredConfig.newConfig() + .sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(apimlSSLContext, hostnameVerifier))); + + SSLContext sslContext = SSLContextBuilder.create() + .loadKeyMaterial(ResourceUtils.getFile(clientKeyStorePath), clientKeyStorePassword, clientKeyPassword, + (Map aliases, Socket socket) -> clientCN) + .loadTrustMaterial(null, trustStrategy).build(); + clientCert = RestAssuredConfig.newConfig() + .sslConfig(new SSLConfig().sslSocketFactory(new SSLSocketFactory(sslContext, hostnameVerifier))); + + } + protected void updateRoutingRules() { applicationEventPublisher.publishEvent(new RefreshRoutesEvent("List of services changed")); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java new file mode 100644 index 0000000000..785e205b33 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/MutateRemoteAddressFilter.java @@ -0,0 +1,44 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.acceptance.xForwardHeaders; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; + +import java.net.InetSocketAddress; + +@Configuration +@Profile("forward-headers-proxy-test") +public class MutateRemoteAddressFilter { + + @Value("${test.proxyAddress}") + public String proxyAddress; + + // A helper filter for tests only that will replace the remote address host in the request so it will appear to come from a remote proxy. + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + WebFilter mutateWebExchangeAddress() { + return (exchange, chain) -> { + ServerWebExchange exchangeFromProxy = exchange.mutate().request( + exchange.getRequest().mutate() + .remoteAddress(new InetSocketAddress(proxyAddress, 0)) + .build() + ).build(); + return chain.filter(exchangeFromProxy); + }; + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java new file mode 100644 index 0000000000..1f92a7a261 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersProxyTest.java @@ -0,0 +1,110 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.acceptance.xForwardHeaders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTest; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.filters.X509awareXForwardedHeadersFilter; + +import java.io.IOException; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNull; + +@AcceptanceTest +@TestPropertySource(properties = { + "apiml.security.forwardHeader.trustedProxies=" +}) +@ActiveProfiles("forward-headers-proxy-test") +class XForwardedHeadersProxyTest extends AcceptanceTestWithMockServices { + + @BeforeEach + void initMockService() throws IOException { + mockService("untrusted-proxies") + .addEndpoint("/untrusted-proxies/xForwardedHeadersCreated") + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER))) + .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_HOST_HEADER))) + .responseCode(SC_OK) + .and() + .addEndpoint("/untrusted-proxies/xForwardedHeadersForwarded") + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER).contains("/test"))) + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER).contains(proxyAddress))) + .responseCode(SC_OK) + .and() + .addEndpoint("/untrusted-proxies/noXForwardedHeadersForwarded") + // All request headers are stripped, and the untrusted proxy is not present in X-forwarded-for + // Note: X_FORWARDED_PREFIX_HEADER is processed differently than in the zuul gateway + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER))) + .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_HOST_HEADER))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.FORWARDED_HEADER))) + .responseCode(SC_OK) + .and() + .start(); + } + + @Test + void whenNoXForwardHeadersInRequest_thenXForwardHeadersCreated() { + given() + .log().all() + .when() + .get(basePath + "/untrusted-proxies/api/v1/xForwardedHeadersCreated") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequest_thenNoXForwardHeadersForwarded() { + given() + .log().all() + .header("x-Forwarded-for", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .header("forwarded", "for=1.1.1.1;prefix=/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/noXForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestFromGW_thenXForwardHeadersForwarded() { + given() + .config(apimlCert) + .log().all() + .header("x-forwarded-For", "1.1.1.1") + .header("X-forwarded-Prefix", "/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestWithClientCert_thenNoXForwardHeadersForwarded() { + given() + .config(clientCert) + .log().all() + .header("x-Forwarded-for", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .header("forwarded", "for=1.1.1.1;prefix=/test") + .when() + .get(basePath + "/untrusted-proxies/api/v1/noXForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java new file mode 100644 index 0000000000..4eee7fd277 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/xForwardHeaders/XForwardedHeadersTrustedProxyTest.java @@ -0,0 +1,99 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.acceptance.xForwardHeaders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTest; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; +import org.zowe.apiml.gateway.filters.X509awareXForwardedHeadersFilter; + +import java.io.IOException; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; + +@AcceptanceTest +@TestPropertySource(properties = { + "apiml.security.forwardHeader.trustedProxies=${test.trustedProxiesPattern}" +}) +@ActiveProfiles("forward-headers-proxy-test") +class XForwardedHeadersTrustedProxyTest extends AcceptanceTestWithMockServices { + + @BeforeEach + void initMockService() throws IOException { + mockService("trusted-proxies") + .addEndpoint("/trusted-proxies/xForwardedHeadersCreated") + .assertion(he -> assertEquals(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER), proxyAddress)) + .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_HOST_HEADER))) + .responseCode(SC_OK) + .and() + .addEndpoint("/trusted-proxies/xForwardedHeadersForwarded") + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_PREFIX_HEADER).contains("/test"))) + .assertion(he -> assertTrue(he.getRequestHeaders().getFirst(X509awareXForwardedHeadersFilter.X_FORWARDED_FOR_HEADER).contains(proxyAddress))) + .responseCode(SC_OK) + .and() + .start(); + } + + + @Test + void whenNoXForwardHeadersInRequest_ThenXForwardHeadersCreated() { + given() + .log().all() + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersCreated") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequest_ThenXForwardedHeadersForwarded() { + given() + .log().all() + .header("X-forwarded-For", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestFromGW_ThenXForwardedHeadersForwarded() { + given() + .config(apimlCert) + .log().all() + .header("x-forwarded-For", "1.1.1.1") + .header("X-forwarded-Prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } + + @Test + void whenXForwardHeadersInRequestWithClientCert_ThenXForwardedHeadersForwarded() { + given() + .config(clientCert) + .log().all() + .header("x-Forwarded-for", "1.1.1.1") + .header("X-forwarded-prefix", "/test") + .when() + .get(basePath + "/trusted-proxies/api/v1/xForwardedHeadersForwarded") + .then() + .statusCode(is(SC_OK)); + } +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java index 8bc5626dcb..a91caae7df 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/AdditionalRegistrationTest.java @@ -89,7 +89,7 @@ public void setUp() throws Exception { ReflectionTestUtils.setField(connectionsConfig, "eurekaServerUrl", "https://host:2222"); ReflectionTestUtils.setField(connectionsConfig, "httpsFactory", httpsFactory); configSpy = Mockito.spy(connectionsConfig); - lenient().doReturn(httpsFactory).when(configSpy).factory(); + lenient().doReturn(httpsFactory).when(configSpy).getHttpsFactory(); lenient().when(httpsFactory.getSslContext()).thenReturn(SSLContexts.custom().build()); lenient().when(httpsFactory.getHostnameVerifier()).thenReturn(new NoopHostnameVerifier()); lenient().when(eurekaFactory.createCloudEurekaClient(any(), any(), clientConfigCaptor.capture(), any(), any(), any())).thenReturn(additionalClientOne, additionalClientTwo); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java index 8765e1862f..4382cd1605 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/CertificateChainServiceTest.java @@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.test.util.ReflectionTestUtils; -import org.zowe.apiml.gateway.config.ConnectionsConfig; +import org.zowe.apiml.security.HttpsConfig; import org.zowe.apiml.security.HttpsConfigError; import org.zowe.apiml.security.SecurityUtils; @@ -32,7 +32,7 @@ class CertificateChainServiceTest { private CertificateChainService certificateChainService; - ConnectionsConfig connectionsConfig = new ConnectionsConfig(null); + HttpsConfig httpsConfig = HttpsConfig.builder().build(); private static final String CERTIFICATE_1 = """ @@ -100,7 +100,7 @@ class GivenValidCertificateChain { void setup() throws CertificateException { certificates[0] = generateCert(CERTIFICATE_1); certificates[1] = generateCert(CERTIFICATE_2); - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(httpsConfig); ReflectionTestUtils.setField(certificateChainService, "certificates", certificates, Certificate[].class); } @@ -120,7 +120,7 @@ void whenGetCertificates_thenPEMIsProduced() { class GivenNoCertificatesInChain { @BeforeEach void setup() { - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(httpsConfig); ReflectionTestUtils.setField(certificateChainService, "certificates", new Certificate[0], Certificate[].class); } @@ -139,7 +139,7 @@ void setup() throws CertificateException { certificates[0] = generateCert(CERTIFICATE_1); certificates[1] = mock(Certificate.class); when(certificates[1].getEncoded()).thenReturn("INVALID_CERT_CONTENT".getBytes()); - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(httpsConfig); ReflectionTestUtils.setField(certificateChainService, "certificates", certificates, Certificate[].class); } @@ -155,7 +155,7 @@ class GivenExceptionDuringChainLoad { @BeforeEach void setup() { - certificateChainService = new CertificateChainService(connectionsConfig); + certificateChainService = new CertificateChainService(httpsConfig); } @Test diff --git a/gateway-service/src/test/resources/application-test.yml b/gateway-service/src/test/resources/application-test.yml index 8a4651d4dc..17386e4de2 100644 --- a/gateway-service/src/test/resources/application-test.yml +++ b/gateway-service/src/test/resources/application-test.yml @@ -26,10 +26,12 @@ spring: allow-bean-definition-overriding: true cloud: gateway: - discovery: - locator: - enabled: false - lowerCaseServiceId: true + server: + webflux: + discovery: + locator: + enabled: false + lower-case-service-id: true application: name: gateway @@ -42,7 +44,7 @@ logging: management: endpoint: gateway: - enabled: true + access: unrestricted endpoints: web: base-path: /application diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml index 256b0dce02..12a6dc28a7 100644 --- a/gateway-service/src/test/resources/application.yml +++ b/gateway-service/src/test/resources/application.yml @@ -29,6 +29,15 @@ server: trustStore: ../keystore/localhost/localhost.truststore.p12 trustStorePassword: password trustStoreType: PKCS12 + # Custom properties for tests onlyAdd commentMore actions + clientKeyStore: ../keystore/client_cert/client-certs.p12 + clientCN: apimtst # case-sensitive + clientKeyStorePassword: password + +#For testing forwarded headers with (un)trusted proxies +test: + proxyAddress: 6.6.6.6 + trustedProxiesPattern: 6\.6\.6\.6 spring: main: @@ -36,10 +45,12 @@ spring: allow-circular-references: true cloud: gateway: - discovery: - locator: - enabled: false - lowerCaseServiceId: true + server: + webflux: + discovery: + locator: + enabled: false + lower-case-service-id: true application: name: gateway @@ -54,7 +65,7 @@ logging: management: endpoint: gateway: - enabled: true + access: unrestricted endpoints: web: base-path: /application diff --git a/gradle/versions.gradle b/gradle/versions.gradle index be61531b50..a81cf5dd80 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -5,31 +5,31 @@ dependencyResolutionManagement { version('projectNode', '20.14.0') version('projectNpm', '10.7.0') - version('springBoot', '3.4.6') - version('springBootGraphQl', '3.4.5') - version('springCloudNetflix', '4.2.1') - version('springCloudCommons', '4.2.1') - version('springCloudCB', '3.2.1') - version('springCloudGateway', '4.2.2') - version('springFramework', '6.2.7') + version('springBoot', '3.5.0') + version('springBootGraphQl', '3.5.0') + version('springCloudNetflix', '4.3.0') + version('springCloudCommons', '4.3.0') + version('springCloudCB', '3.3.0') + version('springCloudGateway', '4.3.0') + version('springFramework', '6.2.8') version('springRetry', '2.0.12') - version('modulith', '1.3.5') + version('modulith', '1.4.0') version('jmolecules', '2023.3.1') version('glassfishHk2', '3.1.1') version('zosUtils', '2.0.6') - version('aws', '1.12.783') + version('aws', '1.12.787') version('awaitility', '4.3.0') - version('bouncyCastle', '1.80') - version('caffeine', '3.2.0') - version('checkerQual', '3.49.3') + version('bouncyCastle', '1.81') + version('caffeine', '3.2.1') + version('checkerQual', '3.49.4') version('commonsLang3', '3.17.0') version('commonsLogging', '1.3.5') version('commonsText', '1.13.1') version('commonsIo', '2.19.0') version('ehCache', '3.10.8') - version('eureka', '2.0.4') + version('eureka', '2.0.5') version('netflixServo', '0.13.2') version('googleErrorprone', '2.38.0') version('gradleGitProperties', '2.5.0') // Used in classpath dependencies @@ -38,7 +38,7 @@ dependencyResolutionManagement { version('hamcrest', '3.0') version('httpClient4', '4.5.14') version('httpClient5', '5.5') - version('infinispan', '15.2.2.Final') + version('infinispan', '15.2.4.Final') version('jacksonCore', '2.19.0') version('jacksonDatabind', '2.19.0') version('jacksonDataformatYaml', '2.19.0') @@ -57,39 +57,39 @@ dependencyResolutionManagement { } version('jbossLogging', '3.6.1.Final') version('jerseySun', '1.19.4') - version('jettyWebSocketClient', '12.0.21') + version('jettyWebSocketClient', '12.0.22') version('jettison', '1.5.4') //0.12.x version contains breaking changes version('jjwt', '0.12.6') version('jodaTime', '2.14.0') version('jsonPath', '2.9.0') version('jsonSmart', '2.5.2') - version('junitJupiter', '5.12.2') - version('junitPlatform', '1.12.2') + version('junitJupiter', '5.13.1') + version('junitPlatform', '1.13.1') version('jxpath', '1.4.0') - version('lettuce', '6.6.0.RELEASE') + version('lettuce', '6.7.1.RELEASE') // force version in build.gradle file - compatibility with Slf4j version('log4j', '2.24.3') version('lombok', '1.18.38') - version('netty', '4.2.1.Final') + version('netty', '4.2.2.Final') // netty reactor contains a bug: https://github.com/reactor/reactor-netty/issues/3559 > https://github.com/reactor/reactor-netty/pull/3581 - version('nettyReactor', '1.2.6') + version('nettyReactor', '1.2.7') version('nimbusJoseJwt', '9.48') - version('openApiDiff', '2.1.1') + version('openApiDiff', '2.1.2') version('picocli', '4.7.7') - version('reactor', '3.7.6') + version('reactor', '3.7.7') version('restAssured', '5.5.5') version('rhino', '1.8.0') - version('springDoc', '2.8.8') + version('springDoc', '2.8.9') version('swaggerCore', '2.2.32') version('swaggerInflector', '2.0.13') version('swagger2Parser', '1.0.75') - version('swagger3Parser', '2.1.28') + version('swagger3Parser', '2.1.29') version('thymeleaf', '3.1.3.RELEASE') version('velocity', '2.4.1') - version('woodstoxCore', '7.1.0') - version('jgit', '7.2.1.202505142326-r') + version('woodstoxCore', '7.1.1') + version('jgit', '7.3.0.202506031305-r') version('gradleNode', '7.1.0') version('sonarGradlePlugin', '5.1.0.4882') version('gradleRelease', '3.1.0') @@ -99,7 +99,7 @@ dependencyResolutionManagement { version('gradleTestLogger', '4.0.0') version('testLogger', '4.0.0') version('micronautPlatform', '4.6.1') - version('micronaut', '4.8.14') + version('micronaut', '4.8.18') version('micronautPlugin', '4.5.3') version('shadow', '8.1.1') version('checkstyle', '10.17.0') diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java index c44dad7735..d03a777ba8 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/GatewayProxyTest.java @@ -38,7 +38,7 @@ class GatewayProxyTest { private static final int SECOND = 1000; private static final int DEFAULT_TIMEOUT = 7 * SECOND; - private static final String HEADER_X_FORWARD_TO = "X-Forward-To"; + static final String HEADER_X_FORWARD_TO = "X-Forward-To"; static ServiceConfiguration conf; diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java new file mode 100644 index 0000000000..5c8522e8b8 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/proxy/XForwardHeadersProxyTest.java @@ -0,0 +1,66 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.integration.proxy; + +import io.restassured.RestAssured; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.GatewayServiceConfiguration; +import org.zowe.apiml.util.config.ItSslConfigFactory; +import org.zowe.apiml.util.config.SslContext; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.zowe.apiml.integration.proxy.GatewayProxyTest.HEADER_X_FORWARD_TO; +import static org.zowe.apiml.util.requests.Endpoints.REQUEST_INFO_ENDPOINT; + +@Tag("GatewayProxyTest") +class XForwardHeadersProxyTest { + + static GatewayServiceConfiguration gwConf; + + static String gwUrl; + + @BeforeAll + static void init() throws Exception { + RestAssured.useRelaxedHTTPSValidation(); + SslContext.prepareSslAuthentication(ItSslConfigFactory.integrationTests()); + + gwConf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + gwUrl = String.format("%s://%s:%s%s", gwConf.getScheme(), gwConf.getHost(), gwConf.getPort(), REQUEST_INFO_ENDPOINT); + } + + @Test + void fromUntrustedProxy_throughGW_xForwardHeadersProvided_untrustedXForwardHeadersForwarded() { + given() + .config(SslContext.clientCertValid) + .header(HEADER_X_FORWARD_TO, "apiml1") + .header("x-forwarded-proto", "http") + .header("x-forwarded-prefix", "/untrusted-proxy") + .header("x-forwarded-port", "666") + .header("x-forwarded-for", "6.6.6.6") + .header("x-forwarded-host", "9.9.9.9") + .when() + .get(gwUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body("headers.x-forwarded-proto", is("https")) + .body("headers.x-forwarded-prefix", is("/dcpassticket/api/v1")) + .body("headers.x-forwarded-port", is(String.valueOf(gwConf.getPort()))) + .body("headers.x-forwarded-for", is(emptyOrNullString())) + .body("headers.x-forwarded-host", not(containsString("9.9.9.9"))); + } + +} diff --git a/zaas-service/src/main/resources/application.yml b/zaas-service/src/main/resources/application.yml index 7887934834..17c9a2becd 100644 --- a/zaas-service/src/main/resources/application.yml +++ b/zaas-service/src/main/resources/application.yml @@ -104,10 +104,7 @@ spring: mvc.enabled: false enabled: false mvc: - throw-exception-if-no-handler-found: false # to suppress NoHandlerFoundException: No handler found for GET /error, we already provide error handling for requests log-resolved-exception: false # Suppress useless logs from AbstractHandlerExceptionResolver - favicon: - enabled: false output: ansi: enabled: detect @@ -148,15 +145,13 @@ management: web: base-path: /application exposure: - include: health,info,shutdown + include: health,info health: defaults: enabled: false endpoint: health: showDetails: always - shutdown: - enabled: true eureka: instance: instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} @@ -200,8 +195,6 @@ eureka: --- spring: config.activate.on-profile: debug - mvc: - throw-exception-if-no-handler-found: true management: endpoints: @@ -209,10 +202,7 @@ management: web: base-path: /application exposure: - include: health,info,routes,loggers,shutdown,hystrixstream,websockets - endpoint: - shutdown: - enabled: true + include: health,info,routes,loggers,hystrixstream,websockets logging: level: diff --git a/zaas-service/src/test/resources/application-test.yml b/zaas-service/src/test/resources/application-test.yml index 2e624a2895..94cff7b54e 100644 --- a/zaas-service/src/test/resources/application-test.yml +++ b/zaas-service/src/test/resources/application-test.yml @@ -57,10 +57,6 @@ spring: client: hostname: ${apiml.service.hostname} ipAddress: ${apiml.service.ipAddress} - mvc: - throw-exception-if-no-handler-found: false # to suppress NoHandlerFoundException: No handler found for GET /error, we already provide error handling for requests - favicon: - enabled: false output: ansi: enabled: detect @@ -142,8 +138,6 @@ eureka: --- spring: config.activate.on-profile: debug - mvc: - throw-exception-if-no-handler-found: true management: endpoints: diff --git a/zaas-service/src/test/resources/application.yml b/zaas-service/src/test/resources/application.yml index a5f9f1ea6b..76652cdcb2 100644 --- a/zaas-service/src/test/resources/application.yml +++ b/zaas-service/src/test/resources/application.yml @@ -67,10 +67,6 @@ spring: client: hostname: ${apiml.service.hostname} ipAddress: ${apiml.service.ipAddress} - mvc: - throw-exception-if-no-handler-found: false # to suppress NoHandlerFoundException: No handler found for GET /error, we already provide error handling for requests - favicon: - enabled: false output: ansi: enabled: detect @@ -153,8 +149,6 @@ eureka: --- spring: config.activate.on-profile: debug - mvc: - throw-exception-if-no-handler-found: true management: endpoints: