Search code examples
javaspring-bootsslhttpsspring-boot-admin

Spring boot admin: Client can't register with admin server over https


I have a handful of spring-boot microservices which register themselves with spring-boot-admin (SBA). When I run the microservices and the SBA server locally, the clients are able to register themselves with the SBA server via HTTP.

When I deploy the apps to the Kubernetes cluster, registration with SBA is done via HTTPS (via an Ingress) I get a javax.net.ssl.SSLHandshakeException in the logs

d.c.b.a.c.r.ApplicationRegistrator : Failed to register application as Application(name=my-app, managementUrl=https://my-app-dev.mydomain.com/actuator, healthUrl=https://my-app-dev.mydomain.com/actuator/health, serviceUrl=https://my-app-dev.mydomain.com) at spring-boot-admin ([https://my-admin-dev.mydomain.com/instances]): I/O error on POST request for "https://my-admin-dev.mydomain.com/instances": Received fatal alert: protocol_version; nested exception is javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version. Further attempts are logged on DEBUG level

In the microservices (the SBA clients) I'm using the following dependency

<dependency>
  <groupId>de.codecentric</groupId>
  <artifactId>spring-boot-admin-starter-client</artifactId>
  <version>2.4.0</version>
</dependency>

And the following in application.yaml

spring.boot.admin.client:
  url: "https://my-admin-dev.mydomain.com"
  instance.service-url: "https://my-app-dev.mydomain.com"

Solution

  • I was able to poke into the spring-boot-admin-starter-client code. Firstly I started with ApplicationRegistrator from the log message which led me to an overridable BlockingRegistrationClient instance (yay!)

    public class SpringBootAdminClientAutoConfiguration {
        ...
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnBean(RestTemplateBuilder.class)
        public static class BlockingRegistrationClientConfig {
            @Bean
            @ConditionalOnMissingBean
            public BlockingRegistrationClient registrationClient(ClientProperties client) {
                RestTemplateBuilder builder = new RestTemplateBuilder().setConnectTimeout(client.getConnectTimeout())
                        .setReadTimeout(client.getReadTimeout());
                if (client.getUsername() != null && client.getPassword() != null) {
                    builder = builder.basicAuthentication(client.getUsername(), client.getPassword());
                }
                return new BlockingRegistrationClient(builder.build());
            }
        }   
    

    Using this post as I guide I was able to create a RestTemplate with the trust store loaded into the SSLContext. I could then override the BlockingRegistrationClient instance with my own which wrapped the custom RestTemplate.

    @Bean
    public BlockingRegistrationClient registrationClient(
            @Value("${ssl.protocol}") String protocol,
            @Value("${ssl.trustStore.path}") String trustStorePath,
            @Value("${ssl.trustStore.password}") String trustStorePassword,
            ClientProperties client) throws Exception {
    
        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(new File(trustStorePath), trustStorePassword.toCharArray())
                .setProtocol(protocol)
                .build();
    
        CloseableHttpClient httpClient = HttpClientBuilder.create()
                .setSSLContext(sslContext)
                .build();
    
        RestTemplateBuilder builder = new RestTemplateBuilder()
                .setConnectTimeout(client.getConnectTimeout())
                .setReadTimeout(client.getReadTimeout())
                .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient));
    
        if (client.getUsername() != null && client.getPassword() != null) {
            builder = builder.basicAuthentication(client.getUsername(), client.getPassword());
        }
        return new BlockingRegistrationClient(builder.build());
    }
    

    application.yaml

    ssl:
      protocol: TLSv1.2
      trustStore:
        path: "/opt/java/openjdk/lib/security/cacerts"
        password: "*****"