Spring Cloud Gateway App throws Exception: No subject alternative DNS name found for an Azure private endpoint

44 views Asked by At

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:

  1. A Spring Cloud Gateway Service that uses Spring Boot and Web-Flux/Netty, currently has an outbound connection to the Virtual Network
  2. 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.

1

There are 1 answers

0
Trec Apps On

The solution turned out to be within the code of the Spring App itself. What was happening was that the HttpClient used by Spring Cloud Gateway for routing was using default "TrustManagers" that indirectly called upon the sun.security.util.HostnameChecker class to inspect the target domain with the provided certificates.

The HostnameChacker's matchAllWildcards is eventually called and contains the following code:

name = name.toLowerCase(Locale.ENGLISH);
        template = template.toLowerCase(Locale.ENGLISH);
        StringTokenizer nameSt = new StringTokenizer(name, ".");
        StringTokenizer templateSt = new StringTokenizer(template, ".");

        if (nameSt.countTokens() != templateSt.countTokens()) {
            return false;
        }

        while (nameSt.hasMoreTokens()) {
            if (!matchWildCards(nameSt.nextToken(),
                        templateSt.nextToken())) {
                return false;
            }
        }
        return true;

Basically, it was trying to compare *.azurewebsites.com with {backend_app_name}.privatelink.azurewebsites.net and 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 HostnameVerifier as I previously thought, but rather in configuring the TrustManager that the HttpClient would use.

In a Configuration Class, I added the following bean method:

    @Bean
    public HttpClientCustomizer TrustCustomizer() {
        return httpClient -> httpClient.secure((SslProvider.SslContextSpec spec) -> {

            try {
                spec.sslContext(SslContextBuilder.forClient()
                        .trustManager(new AzureTrustManager()).build());
            } catch (SSLException e) {
                throw new RuntimeException(e);
            }
        });
    }

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 HttpClientCustomizer to Customize the HttpClient to use my TrustManager (not using the InsecureTrustManagerFactory, I opted to create my own)

To implement my TrustManager and to ensure I had access to the peer host that the client actually called, it had to extend the X509ExtendedTrustManager as the hostname needed is found in SSLEngine::getPeerHost()


public class AzureTrustManager extends X509ExtendedTrustManager {

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {

    // Note: Not recommended in any public library, I was just interested in the peerhost

        checkServerTrusted(x509Certificates,sslEngine.getPeerHost());
    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
        
    // If this class merely implemented 'X509TrustManager', it would have been wrapped by the 'X509TrustManagerWrapper' class
    // and the 's' param would have been 'UNKNOWN'

           // Prepare to collect all of the CNs in the available certificates
           List<List<String>> cnNames = new ArrayList<>(x509Certificates.length);

        for(X509Certificate cert: x509Certificates){
            cert.checkValidity();
            List<String> nameList = getCn(cert);
            if(!validateDomains(nameList))
                throw new CertificateException("Found Invalid Certificate Name");
            cnNames.add(nameList);
        }

        boolean found = false;
        String[] sSplit = s.split("\\.");
        for(int c = 0; !found && c < cnNames.size(); c++){
            found = foundMatch(cnNames.get(c), sSplit);
        }

        // If we have not found a match, then our connection is invalid
        if(!found)
        {
            throw new CertificateException(String.format("No subject alternative DNS name matching %s found.", s));
        }
    
    }

    // Implementation of 'validateDomains', 'foundMatch', 'getCN', and the other methods required in a 'X509ExtendedTrustManager'

}

The implementation for getCN was loosely inspired by https://www.baeldung.com/java-extract-common-name-x509-certificate

After 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.