I'm trying to construct a basic architecture on Microsoft Azure. Right now, I'm focused on 2 apps that I'm trying to connect via Private Endpoint across a Virtual Network:
- A Spring Cloud Gateway Service that uses Spring Boot and Web-Flux/Netty, currently has an outbound connection to the Virtual Network
- A Spring Boot Back-end Service that has a private endpoint through the Virtual Network.
Both Apps are currently accessible and I can call the actuator endpoint on both successfully (I plan to one day change this for the back-end service). They are connected to the same virtual network.
Here is the Code in the gateway service that forwards any request to the back-end service (or was intended to):
@Bean
public RouteLocator tcTestRoutes(RouteLocatorBuilder builder)
{
return builder.routes()
.route((PredicateSpec ps) ->
ps.path("/User-api/**")
.filters((GatewayFilterSpec filter) -> filter.stripPrefix(1))
.uri("https://{backend_app_name}.privatelink.azurewebsites.net"))
.build();
}
Calling https://{gateway_app_name}.azurewebsites.net/actuator works.
Calling https://{backend_app_name}.azurewebsites.net/actuator works.
Calling https://{gateway_app_name}.azurewebsites.net/User-api/actuator should give me the same response as calling https://{backend_app_name}.azurewebsites.net/actuator. But I get an error page.
Looking at the Log Stream, I'm able to see
2024-02-15T16:39:54.020457970Z: [INFO] 2024-02-15T16:39:53.976Z WARN 116 --- [or-http-epoll-4] r.netty.http.client.HttpClientConnect : [32e3c331, L:/169.254.254.2:47760 - R:{backend_app_name}.privatelink.azurewebsites.net/10.0.3.20:443] The connection observed an error
2024-02-15T16:39:54.020489271Z: [INFO]
2024-02-15T16:39:54.020495571Z: [INFO] javax.net.ssl.SSLHandshakeException: No subject alternative DNS name matching {backend_app_name}.privatelink.azurewebsites.net found.
2024-02-15T16:39:54.020501172Z: [INFO] at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131) ~[na:na]
2024-02-15T16:39:54.020506472Z: [INFO] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
2024-02-15T16:39:54.020511072Z: [INFO] Error has been observed at the following site(s):
2024-02-15T16:39:54.020515572Z: [INFO] *__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
2024-02-15T16:39:54.020521172Z: [INFO] *__checkpoint ⇢ HTTP GET "/User-api/actuator" [ExceptionHandlingWebHandler]
and
2024-02-15T16:39:54.020722982Z: [INFO] Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching {backend_app_name}.privatelink.azurewebsites.net found.
2024-02-15T16:39:54.020727582Z: [INFO] at java.base/sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:212) ~[na:na]
2024-02-15T16:39:54.020732082Z: [INFO] at java.base/sun.security.util.HostnameChecker.match(HostnameChecker.java:103) ~[na:na]
From the logs, it is clear that the private DNS is resolving to the correct IP Address, but I'm not sure how to ensure that HTTPS works (I'd rather not use plain-HTTP) - or if this is something I need to fix withing my app code or somewhere within the App Service Settings.
The solution turned out to be within the code of the Spring App itself. What was happening was that the
HttpClientused by Spring Cloud Gateway for routing was using default "TrustManagers" that indirectly called upon thesun.security.util.HostnameCheckerclass to inspect the target domain with the provided certificates.The HostnameChacker's matchAllWildcards is eventually called and contains the following code:
Basically, it was trying to compare
*.azurewebsites.comwith{backend_app_name}.privatelink.azurewebsites.netand because the resulting token count of three and four (respectively) is not equal, the validation would fail causing the issue as observed. Had the Microsoft Certificate featured*.privatelink.azurewebsites.net, I would not have run into this issue.The solution was not in trying to replace the
HostnameVerifieras I previously thought, but rather in configuring theTrustManagerthat theHttpClientwould use.In a Configuration Class, I added the following bean method:
Inspired by https://medium.com/@m1326318/configuring-spring-cloud-gateway-ced5dae663bb and an answer found here: How to provide an all-trusting SslProvider in netty? , I use the
HttpClientCustomizerto Customize theHttpClientto use myTrustManager(not using theInsecureTrustManagerFactory, I opted to create my own)To implement my
TrustManagerand to ensure I had access to the peer host that the client actually called, it had to extend theX509ExtendedTrustManageras the hostname needed is found inSSLEngine::getPeerHost()The implementation for
getCNwas loosely inspired by https://www.baeldung.com/java-extract-common-name-x509-certificateAfter adding a logging call with the peerhost string and running the app in the Azure App Service, I was able to observe said log in the Log Stream and thus, can confirm this solution works.