3
votes

I want the following encrypted setup for a microservice environment. A SSL enabled Eureka Server with Cloud Configuration Service and multiple microservices that only need to connect to the Discovery service, read the configuration and run their stuff. Everything via HTTPS and with authentication.

So...what I did:

A Eureka Discovery Server including a Cloud Configuration Service on /config/* The Server is registering itself to the Eureka server and the Eureka Dashboard shows the instance. This service runs on port 9001 with SSL enabled and user-authentication. The service works as usual.

Then I've created a new SpringBoot service, that connects to Eureka and registered itself to it. Because of the self signed certificate, I wrote a small SSLConfguration Class from this post: How to override Spring Cloud Eureka default discovery client default ssl context? to give my personal truststore to the underlying EurekaClient. All our services are configured with application.yml:

spring:
    application:
        name: mark2

server:
    port: 9998
    ssl:
        enabled: true
        key-store: classpath:keystore.p12
        key-store-password: changeit
        key-store-type: PKCS12
        keyAlias: mark2

http:
    client:
      ssl:
          trust-store: classpath:keystore.p12
          trust-store-password: changeit

eureka:
    client:
        serviceUrl:
            defaultZone: https://user:pass@localhost:9001/eureka

The client connects and registers perfectly.

Now I want the SpringBoot service to register to the Eureka Server and load the configuration from the Config Server - during bootstrap. So I've moved the spring, http and eureka settings to bootstrap.yml

spring:
    application:
        name: mark2
    cloud:
        config:
            username: user
            password: password
            discovery:
                enabled: true
                serviceId: EUREKA-DISCOVERY-SERVICE
            failFast: true
            name: my
            profile: settings
#            label: v1

http:
    client:
      ssl:
          trust-store: classpath:keystore.p12
          trust-store-password: changeit

eureka:
    client:
        serviceUrl:
            defaultZone: https://user:password@localhost:9001/eureka
    instance:
        metadataMap:
            user: user
            password: password
            configPath: /config

If the service starts, it tries to connect during bootstrap to the Eureka server. This doesn't work, because it isn't trusting the self signed certificate from the Eureka server. I cannot use @Configure or @Bean because I'm in the bootstrap (at least nothing that I found worked). The above solution with the SSLConfiguration isn't working too.

Ok, then I set the JVM args with javax.net.ssl.trustStore etc. to my personal truststore.

bootRun {
   jvmArgs = [ 
     "-Djavax.net.ssl.trustStore=/absolute/path/to/keystore.p12", 
     "-Djavax.net.ssl.trustStorePassword=changeit", 
     "-Djavax.net.ssl.trustStoreType=PKCS12", 
   ]
}

The service starts and the Eureka client starts to communicate with the Eureka server, finds a Cloud Configuration instance and loads a property file and the Spring context is created. The property contains only some test properties.

But then - error:

Field optionalArgs in org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration$RefreshableEurekaClientConfiguration required a single bean, but 2 were found:
    - getTrustStoredEurekaClient: defined by method 'getTrustStoredEurekaClient' in class path resource [de/mark2/SslConfiguration.class]
    - discoveryClientOptionalArgs: defined by method 'discoveryClientOptionalArgs' in org.springframework.cloud.netflix.eureka.config.DiscoveryClientOptionalArgsConfiguration

Ok, bad but ok. Because we gave the JVM args, we should not use SSLConfiguration any more. Removed.

Now - everything seems ok and the service should start the embedded Tomcat. But this doesn't work because of another Exception that says, that the configured keystore is empty ("the trustAnchors parameter must be non-empty" means the keystore exists but doesn't contain any alias certs). I think, the JVM args are used in the embedded Tomcat and it tries to use the truststore for some kind of ClientCertificates or so. I don't know.

If I disable the cloud configuration - the embedded Tomcat still has the same error. If I then remove the JVM args too - the service comes up.

So - I don't have an idea, what I need to do, to communicate encrypted during bootstrap without defining the JVM args for the Eureka Client or give the embedded tomcat a Truststore (which he doesn't need).

3

3 Answers

2
votes

Define META-INF/spring.factories and add a org.springframework.cloud.bootstrap.BootstrapConfiguration = ... line

The class could be like:

@Configuration
@BootstrapConfiguration
public class SslConfiguration {
  @Value("${http.client.ssl.trust-store}")
  private URL trustStore;
  @Value("${http.client.ssl.trust-store-password}")
  private String trustStorePassword;

  @Bean
  public DiscoveryClient.DiscoveryClientOptionalArgs getTrustStoredEurekaClient(SSLContext sslContext) {
    DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient.DiscoveryClientOptionalArgs();
    args.setSSLContext(sslContext);
    return args;
  }

  @Bean
  public SSLContext sslContext() throws Exception {
    return new SSLContextBuilder().loadTrustMaterial(trustStore, trustStorePassword.toCharArray()).build();
  }
}

And because the DiscoveryClientOptionalArgs are now defined twice, then add another class, which is loaded after the Spring Context is up

@Configuration
public class DiscoveryServiceConfiguration {
  @Bean
  public static BeanFactoryPostProcessor registerPostProcessor() {
    return (ConfigurableListableBeanFactory beanFactory) -> {
      BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
      for (String beanDefinitionName : registry.getBeanDefinitionNames()) {
        if (beanDefinitionName.equalsIgnoreCase("discoveryClientOptionalArgs")) {
          BeanDefinition beanDefinition = registry.containsBeanDefinition(beanDefinitionName) ? registry.getBeanDefinition(beanDefinitionName) : null;
          if (beanDefinition != null) {
            if (registry.containsBeanDefinition(beanDefinitionName)) {
              registry.removeBeanDefinition(beanDefinitionName);
            }
          }
        }
      }
    };
  }
}

Then it works too. You don't need to define JVM args anymore.

1
votes

interesting thing - If I use embedded Undertow instead of embedded Tomcat, it works - but is this the solution?

dependencies {
  compile("org.springframework.boot:spring-boot-starter") {
    exclude module: "tomcat-embed-el"
  }
  compile("org.springframework.boot:spring-boot-starter-web") {
    exclude module: "spring-boot-starter-tomcat"
  }
  compile('org.springframework.boot:spring-boot-starter-undertow')
  ...
}
0
votes

For those who found this topic in 2022. You can setup TLS using eureka.client.tls properties. It is strange that these properties are undocumented.

For example:

eureka:
  client:
    secure-port-enabled: true
    secure-port: 8070
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: https://eureka-server:8070/eureka
    tls:
      enabled: true
      keyStore: classpath:app.jks
      trustStore: classpath:app.jks
      keyStoreType: JKS
      keyStorePassword: '{cipher}ede988d987a9ece2fc84579bdc7159cce47cd0ae462a7ba'
      trustStorePassword: '{cipher}ede988d987a9ece2fc84579bdc7159cce47cd0ae462a7ba'
      keyPassword: '{cipher}ede988d987a9ece2fc84579bdc7159cce47cd0ae462a7ba'