How to override Spring Cloud Eureka default discovery client default ssl context?
Asked Answered
R

2

6

I'm trying to enable https for spring cloud eureka server. Yaml config:

server:
  port: 8100
ssl:
  clientAuth: want
  protocol: TLS
  key-store: classpath:keystore/keystore.jks
  key-store-password: some
  key-password: some
eureka:
  instance:
    prefer-ip-address: true
    non-secure-port-enabled: false
    secure-port-enabled: true
    secure-port: ${server.port}
    healthCheckUrl: https://${eureka.hostname}:${secure-port}/health
    statusPageUrl: https://${eureka.hostname}:${secure-port}/info
    homePageUrl: https://${eureka.hostname}:${secure-port}/
security:
  basic:
    enabled: true

Then I start a client to register into the server. I haven't import the self-signed cert thus of cource I get the excpetion sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target. I don't want to import cert because there are plenty instances and certs management cost much. Thus I put the cert in classpath and load it during start up. I override the client default ssl context and ssl socket facotory by adding codes

SSLContext.setDefault(sslContext);
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

It works fine when it calls other eureka client using feign. However it has no effort when registering to eureka server. I check the source code and find that eureka discovery client using jersey, jersey call the http apache client. The trouble is, it use SchemeRegistryFactory.createDefault() which will final call SSLContexts.createDefault() who will not taken system properties into consideration. In other words, this http client will not token my custom SSLContexts. So my question is, it there a method to add/resigter/replace the default http client in eureka discovery client?

Ringer answered 17/2, 2017 at 4:4 Comment(1)
the above code works well, if the Spring context is up and running, but when you want to use the Eureka client during bootstrap f.e. to connect the Cloud Config service, then it doesn't work. There is no really good solution, but one way is putting the following into META-INF/spring.factories: org.springframework.cloud.bootstrap.BootstrapConfiguration = ...SslConfiguration and the certificate problem is solved too.Accusal
R
9

Finally I found a solution after digging the source code many times. I'm using version Camden.SR5 which will call eureka-client-1.4.12.

If you provide a EurekaJerseyClient in DiscoveryClientOptionalArgs, then the Discovery client will not intial the default one. Part of the code from class DiscoveryClient.

private void scheduleServerEndpointTask(EurekaTransport eurekaTransport,
                                        DiscoveryClientOptionalArgs args) {
...

    EurekaJerseyClient providedJerseyClient = args == null
            ? null
            : args.eurekaJerseyClient;

    eurekaTransport.transportClientFactory = providedJerseyClient == null
            ? TransportClientFactories.newTransportClientFactory(clientConfig, additionalFilters, applicationInfoManager.getInfo())
            : TransportClientFactories.newTransportClientFactory(additionalFilters, providedJerseyClient);
...
}

Then I add a class to make a DiscoveryClientOptionalArgs bean.

import com.netflix.discovery.DiscoveryClient;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient;
import com.qy.insurance.cloud.core.eureka.CustomEurekaJerseyClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class EurekaSslConfig {

@Value("${eureka.client.service-url.defaultZone}")
private String defaultZone;

@Autowired
private EurekaClientConfig config;

@Autowired
private DefaultSslConfig defaultSslConfig;

@Bean
public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs(){
    if(!defaultSslConfig.isFinish()){
        log.warn("Default SSLContext might not have been updated! Please check!");
    }

    DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient.DiscoveryClientOptionalArgs();

    CustomEurekaJerseyClientBuilder clientBuilder = new CustomEurekaJerseyClientBuilder()
            .withClientName("DiscoveryClient-HTTPClient-Custom")
            .withUserAgent("Java-EurekaClient")
            .withConnectionTimeout(config.getEurekaServerConnectTimeoutSeconds() * 1000)
            .withReadTimeout(config.getEurekaServerReadTimeoutSeconds() * 1000)
            .withMaxConnectionsPerHost(config.getEurekaServerTotalConnectionsPerHost())
            .withMaxTotalConnections(config.getEurekaServerTotalConnections())
            .withConnectionIdleTimeout(config.getEurekaConnectionIdleTimeoutSeconds() * 1000)
            .withEncoderWrapper(CodecWrappers.getEncoder(config.getEncoderName()))
            .withDecoderWrapper(CodecWrappers.resolveDecoder(config.getDecoderName(), config.getClientDataAccept()));
    if (defaultZone.startsWith("https://")) {
        clientBuilder.withSystemSSLConfiguration();
    }

    EurekaJerseyClient jerseyClient = clientBuilder.build();
    args.setEurekaJerseyClient(jerseyClient);//Provide custom EurekaJerseyClient to override default one
    return args;
}

}

To insure my custom EurekaJerseyClient working well, I forked the code from EurekaJerseyClientImpl and made some modification.

import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.converters.wrappers.DecoderWrapper;
import com.netflix.discovery.converters.wrappers.EncoderWrapper;
import com.netflix.discovery.provider.DiscoveryJerseyProvider;
import com.netflix.discovery.shared.MonitoredConnectionManager;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl;
import com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter;
import com.netflix.discovery.util.DiscoveryBuildInfo;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.client.apache4.config.ApacheHttpClient4Config;
import com.sun.jersey.client.apache4.config.DefaultApacheHttpClient4Config;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.conn.SchemeRegistryFactory;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.TextUtils;

public class CustomEurekaJerseyClientBuilder {
private boolean systemSSL;
private String clientName;
private int maxConnectionsPerHost;
private int maxTotalConnections;
private String trustStoreFileName;
private String trustStorePassword;
private String userAgent;
private String proxyUserName;
private String proxyPassword;
private String proxyHost;
private String proxyPort;
private int connectionTimeout;
private int readTimeout;
private int connectionIdleTimeout;
private EncoderWrapper encoderWrapper;
private DecoderWrapper decoderWrapper;

public CustomEurekaJerseyClientBuilder withClientName(String clientName) {
    this.clientName = clientName;
    return this;
}

public CustomEurekaJerseyClientBuilder withUserAgent(String userAgent) {
    this.userAgent = userAgent;
    return this;
}

public CustomEurekaJerseyClientBuilder withConnectionTimeout(int connectionTimeout) {
    this.connectionTimeout = connectionTimeout;
    return this;
}

public CustomEurekaJerseyClientBuilder withReadTimeout(int readTimeout) {
    this.readTimeout = readTimeout;
    return this;
}

public CustomEurekaJerseyClientBuilder withConnectionIdleTimeout(int connectionIdleTimeout) {
    this.connectionIdleTimeout = connectionIdleTimeout;
    return this;
}

public CustomEurekaJerseyClientBuilder withMaxConnectionsPerHost(int maxConnectionsPerHost) {
    this.maxConnectionsPerHost = maxConnectionsPerHost;
    return this;
}

public CustomEurekaJerseyClientBuilder withMaxTotalConnections(int maxTotalConnections) {
    this.maxTotalConnections = maxTotalConnections;
    return this;
}

public CustomEurekaJerseyClientBuilder withProxy(String proxyHost, String proxyPort, String user, String password) {
    this.proxyHost = proxyHost;
    this.proxyPort = proxyPort;
    this.proxyUserName = user;
    this.proxyPassword = password;
    return this;
}

public CustomEurekaJerseyClientBuilder withSystemSSLConfiguration() {
    this.systemSSL = true;
    return this;
}

public CustomEurekaJerseyClientBuilder withTrustStoreFile(String trustStoreFileName, String trustStorePassword) {
    this.trustStoreFileName = trustStoreFileName;
    this.trustStorePassword = trustStorePassword;
    return this;
}

public CustomEurekaJerseyClientBuilder withEncoder(String encoderName) {
    return this.withEncoderWrapper(CodecWrappers.getEncoder(encoderName));
}

public CustomEurekaJerseyClientBuilder withEncoderWrapper(EncoderWrapper encoderWrapper) {
    this.encoderWrapper = encoderWrapper;
    return this;
}

public CustomEurekaJerseyClientBuilder withDecoder(String decoderName, String clientDataAccept) {
    return this.withDecoderWrapper(CodecWrappers.resolveDecoder(decoderName, clientDataAccept));
}

public CustomEurekaJerseyClientBuilder withDecoderWrapper(DecoderWrapper decoderWrapper) {
    this.decoderWrapper = decoderWrapper;
    return this;
}

public EurekaJerseyClient build() {
    MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
    try {
        return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
    } catch (Throwable e) {
        throw new RuntimeException("Cannot create Jersey client ", e);
    }
}

class MyDefaultApacheHttpClient4Config extends DefaultApacheHttpClient4Config {

    private static final String PROTOCOL = "https";
    private static final String PROTOCOL_SCHEME = "SSL";
    private static final int HTTPS_PORT = 443;
    private static final String KEYSTORE_TYPE = "JKS";

    MyDefaultApacheHttpClient4Config() {
        MonitoredConnectionManager cm;

        if (systemSSL) {
            cm = createSystemSslCM();
        } else {
            cm = createDefaultSslCM();
        }

        if (proxyHost != null) {
            addProxyConfiguration(cm);
        }

        DiscoveryJerseyProvider discoveryJerseyProvider = new DiscoveryJerseyProvider(encoderWrapper, decoderWrapper);
        getSingletons().add(discoveryJerseyProvider);

        // Common properties to all clients
        cm.setDefaultMaxPerRoute(maxConnectionsPerHost);
        cm.setMaxTotal(maxTotalConnections);
        getProperties().put(ApacheHttpClient4Config.PROPERTY_CONNECTION_MANAGER, cm);

        String fullUserAgentName = (userAgent == null ? clientName : userAgent) + "/v" + DiscoveryBuildInfo.buildVersion();
        getProperties().put(CoreProtocolPNames.USER_AGENT, fullUserAgentName);

        // To pin a client to specific server in case redirect happens, we handle redirects directly
        // (see DiscoveryClient.makeRemoteCall methods).
        getProperties().put(ClientConfig.PROPERTY_FOLLOW_REDIRECTS, Boolean.FALSE);
        getProperties().put(ClientPNames.HANDLE_REDIRECTS, Boolean.FALSE);
    }

    private void addProxyConfiguration(MonitoredConnectionManager cm) {
        if (proxyUserName != null && proxyPassword != null) {
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_USERNAME, proxyUserName);
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_PASSWORD, proxyPassword);
        } else {
            // Due to bug in apache client, user name/password must always be set.
            // Otherwise proxy configuration is ignored.
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_USERNAME, "guest");
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_PASSWORD, "guest");
        }
        getProperties().put(DefaultApacheHttpClient4Config.PROPERTY_PROXY_URI, "http://" + proxyHost + ":" + proxyPort);
    }

    private MonitoredConnectionManager createSystemSslCM() {
        MonitoredConnectionManager cm;
        X509HostnameVerifier hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
        SSLConnectionSocketFactory systemSocketFactory = new SSLConnectionSocketFactory(
                (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
                split(System.getProperty("https.protocols")),
                split(System.getProperty("https.cipherSuites")),
                hostnameVerifier);
        SSLSocketFactory sslSocketFactory = new SSLSocketFactoryAdapter(systemSocketFactory);
        SchemeRegistry sslSchemeRegistry = new SchemeRegistry();
        sslSchemeRegistry.register(new Scheme(PROTOCOL, HTTPS_PORT, sslSocketFactory));
        cm = new MonitoredConnectionManager(clientName, sslSchemeRegistry);
        return cm;
    }

    /**
     * @see SchemeRegistryFactory#createDefault()
     */
    private MonitoredConnectionManager createDefaultSslCM() {
        final SchemeRegistry registry = new SchemeRegistry();
        registry.register(
                new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
        registry.register(
                new Scheme("https", 443, new SSLSocketFactoryAdapter(SSLConnectionSocketFactory.getSocketFactory())));
        return new MonitoredConnectionManager(clientName, registry);
    }

    private String[] split(final String s) {
        if (TextUtils.isBlank(s)) {
            return null;
        }
        return s.split(" *, *");
    }
}
}

Hope this could helpe those who is not easy to import cert in Production JVM just like me.

Ringer answered 17/2, 2017 at 10:35 Comment(0)
D
5

I managed to inject ssl-context to the eureka client in Finchley.M9 spring cloud as following:

@Configuration
public class SslConfiguration {

    private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);

    @Value("${http.client.ssl.trust-store}")
    private File 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 {
        logger.info("initialize ssl context bean with keystore {} ", trustStore);
        return new SSLContextBuilder()
                .loadTrustMaterial(
                        trustStore,
                        trustStorePassword.toCharArray()
                ).build();
    }
}
Dissimilitude answered 23/4, 2018 at 19:3 Comment(3)
saved my day! Still works with org.springframework.cloud:spring-cloud-starter-netflix-eureka-client -> 2.2.2.RELEASEGenny
This worked well for me using 2.2.3.RELEASE.... then I upgraded to 2.6.1 and all hell has broken loose again :(Suu
um ... i think one should mention that this solution is somewhat based on luck - you're injecting SSLContext but also provide a bean of that; Spring will by default (!) inject "local" beans first but will also revert to injecting eligible instances which are present elsewhere - for instance: it is very possible to have SSLContext instantiated automatically via SSLContext#getDefault, which used in many, many classes everywhere ... you really should use spring bean-names and reference them via @Qualifier - then and only then your code may be somewhat stableFollansbee

© 2022 - 2024 — McMap. All rights reserved.