reactor / reactor-netty

TCP/HTTP/UDP/QUIC client/server with Reactor over Netty
https://projectreactor.io
Apache License 2.0
2.6k stars 645 forks source link

Does the WebClient not respect the default JVM SSLContext? #640

Closed ealexhaywood closed 5 years ago

ealexhaywood commented 5 years ago

Expected behavior

When I pass keystore/truststore arguments, such as

-Djavax.net.ssl.keyStore=$(KEYSTORE) -Djavax.net.ssl.keyStorePassword=$(PASSWORD) -Djavax.net.ssl.trustStore=$(TRUSTSTORE) -Djavax.net.ssl.trustStorePassword=$(TRUSTSTORE_PASSWORD) -Djavax.net.ssl.trustStoreType=$(TRUSTSTORE_TYPE) in the _JAVA_OPTIONS environment variable to the JVM, I expected the WebClient to use the default Java SSLContext.

Actual behavior

It does not use the default SSLContext set by the JVM, resulting in handshake_failures.

Steps to reproduce

Pass a similar _JAVA_OPTIONS variable and try to make a request to a trusted server configured for 2-way TLS.

Here's how I am using the web client:

WebClient.create(baseUrl)
  .post()
  .uri(uriBuilder -> uriBuilder.path(someUrl).build())
  // Other irrelevant configurations, e.g. headers, contentType, body, exchange, etc.
  .flatMap(response -> {
     return response.bodyToMono(Void.class);
  })
  .block();

Reactor Netty version

0.8.4.RELEASE

JVM version (e.g. java -version)

openjdk version "11" 2018-09-25 OpenJDK Runtime Environment 18.9 (build 11+28) OpenJDK 64-Bit Server VM 18.9 (build 11+28, mixed mode)

OS version (e.g. uname -a)

JVM ran in an official openJDK docker container -- openjdk:11-jre-slim

violetagg commented 5 years ago

@ealexhaywood Can you try setting -Dio.netty.handler.ssl.noOpenSsl=true

ealexhaywood commented 5 years ago

That didn't work :( it still sends credential-less requests.

I'm fine with using an SSL context builder, but it would have been nice to somehow just use the default context provided by the JVM similar to the Apache HttpClient's [useSystemProperties()](http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/HttpClientBuilder.html#useSystemProperties())

violetagg commented 5 years ago

Did you at least enable the SSL?

https://docs.spring.io/spring/docs/5.1.1.RELEASE/spring-framework-reference/web-reactive.html#webflux-client-builder-reactor

HttpClient httpClient = HttpClient.create().secure(spec ->
   spec.sslContext(SslContextBuilder.forClient().build()));

    WebClient webClient = WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
ealexhaywood commented 5 years ago

Right, that was kind of my point. I didn't think I would have to set an SSL context since the JDK's native web clients URLConnection and HttpClient don't require you to, and was wondering if there was a non-code changing way to do this.

And that did not work unfortunately.

violetagg commented 5 years ago

@rstoyanchev What do you think? ^^^ Should there be some SSL configuration enabled by default for WebClient?

bclozel commented 5 years ago

We could indeed enable security by default on the WebClient Reactor connector (we've enabled compression already).

It looks like this won't solve this particular issue; looking at Netty's codebase, it doesn't seem Netty is looking at javax.net.ssl system properties when setting up the SSL contexts.

ealexhaywood commented 5 years ago

I meant to post back a while back to show how I configured the WebClient for TLS for anyone else running into a similar problem, for what it's worth:

HttpClient httpClient = HttpClient.create().secure(spec ->
{
  try {
    String keyStoreLocation = System.getProperty("javax.net.ssl.keyStore");
    String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword");
    String trustStoreLocation = System.getProperty("javax.net.ssl.trustStore");
    String trustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword");

    KeyStore keyStore = KeyStore.getInstance("JKS");
    keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStoreLocation)), keyStorePassword.toCharArray());

    // Set up key manager factory to use our key store
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());

    // truststore
    KeyStore trustStore = KeyStore.getInstance("JKS");
    trustStore.load(new FileInputStream((ResourceUtils.getFile(trustStoreLocation))), trustStorePassword.toCharArray());

    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(trustStore);

    spec.sslContext(SslContextBuilder.forClient()
      .keyManager(keyManagerFactory)
      .trustManager(trustManagerFactory)
      .build());
  } catch (Exception e) {
    LOGGER.warn("Unable to set SSL Context", e); 
  }
});

WebClient webClient = WebClient.builder()
  .baseUrl(baseUrl)
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();

Like I said, not a big deal at all. Just would be nice to have :)

violetagg commented 5 years ago

@ealexhaywood Why do you need to do this by yourself? You can delegate that to Netty.

I did the following:

-Djavax.net.ssl.keyStore=<path>/keystore.jks
-Djavax.net.ssl.keyStorePassword=<password>
-Djavax.net.ssl.trustStore=<path>/truststore.ts
-Djavax.net.ssl.trustStorePassword=<password>
-Djavax.net.ssl.trustStoreType=JKS
-Dio.netty.handler.ssl.noOpenSsl=true
WebClient.builder()
         .clientConnector(new ReactorClientHttpConnector(
             HttpClient.create()
                       .secure(spec -> spec.sslContext(SslContextBuilder.forClient()))))
         .build()
         .get()
         .uri("https://localhost:8080/text")
         .retrieve()
         .bodyToMono(String.class)
         .block()
ealexhaywood commented 5 years ago

@violetagg Did you pass those arguments as a _JAVA_OPTIONS environment variable? Maybe it works as regular commandline argument.

In our environment there is a strong preference to use environment variables for configuration instead of arguments.

violetagg commented 5 years ago

@ealexhaywood JVM options

ealexhaywood commented 5 years ago

Right, which is different from setting the _JAVA_OPTIONS variable.

voodemsanthosh commented 4 years ago

I have tried by providing solution and getting below error for https://localhost:8080

io.netty.handler.codec.DecoderException: javax.net.ssl.SSLHandshakeException: no cipher suites in common

And I used http://locahost:8080 and getting below,

Caused by: io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record

voodemsanthosh commented 4 years ago

I have tried by providing solution and getting below error for https://localhost:8080

io.netty.handler.codec.DecoderException: javax.net.ssl.SSLHandshakeException: no cipher suites in common

And I used http://locahost:8080 and getting below,

Caused by: io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record

Sorry my bad. I have set server.ssl properties in config file which forcing to enable localhost also secure.

SkotnikovD commented 4 years ago

@ealexhaywood Why do you need to do this by yourself? You can delegate that to Netty.

I did the following:

-Djavax.net.ssl.keyStore=<path>/keystore.jks
-Djavax.net.ssl.keyStorePassword=<password>
-Djavax.net.ssl.trustStore=<path>/truststore.ts
-Djavax.net.ssl.trustStorePassword=<password>
-Djavax.net.ssl.trustStoreType=JKS
-Dio.netty.handler.ssl.noOpenSsl=true
WebClient.builder()
         .clientConnector(new ReactorClientHttpConnector(
             HttpClient.create()
                       .secure(spec -> spec.sslContext(SslContextBuilder.forClient()))))
         .build()
         .get()
         .uri("https://localhost:8080/text")
         .retrieve()
         .bodyToMono(String.class)
         .block()

Seemes like a good solution, but what should I provide in 'path' for keystore, if my app is spring boot app and it's packed in jar (or if )? How to reference my keystore with relevant path? Seems like classpath:client.jks doesn't work, because TrustStoreManager acts like String storePropName = System.getProperty("javax.net.ssl.trustStore", jsseDefaultStore); ... new File(storePropName ); Wondering why they didn't use getResources ...

But it worked in @ealexhaywood manual setup solution, as he uses ResourceUtils.getFile

mplain commented 4 years ago

@ealexhaywood Why do you need to do this by yourself? You can delegate that to Netty.

I did the following:

-Djavax.net.ssl.keyStore=<path>/keystore.jks
-Djavax.net.ssl.keyStorePassword=<password>
-Djavax.net.ssl.trustStore=<path>/truststore.ts
-Djavax.net.ssl.trustStorePassword=<password>
-Djavax.net.ssl.trustStoreType=JKS
-Dio.netty.handler.ssl.noOpenSsl=true
WebClient.builder()
         .clientConnector(new ReactorClientHttpConnector(
             HttpClient.create()
                       .secure(spec -> spec.sslContext(SslContextBuilder.forClient()))))
         .build()
         .get()
         .uri("https://localhost:8080/text")
         .retrieve()
         .bodyToMono(String.class)
         .block()

I tried that, and trustStore gets loaded, however keyStore does not SSLContextImpl.engineInit() loads system properties if it receives null for trustManager, but it does not do so for keyManager

rit0m commented 1 year ago
public WebClient getOauthWebClient() {
    final SslContext sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build();
    final HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(sslContext));
    return WebClient.builder()
            .filter(oAuthErrorHandler())
            .clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}

getting

io.netty.handler.ssl.SslHandshakeTimeoutException: handshake timed out after 10000ms
Tony92izi2 commented 6 months ago

Hi guys , I know its a bit late but this is a simplest solution:

-Djavax.net.ssl.keyStore=/keystore.jks -Djavax.net.ssl.keyStorePassword= -Djavax.net.ssl.trustStore=/truststore.ts -Djavax.net.ssl.trustStorePassword= -Djavax.net.ssl.trustStoreType=JKS

SslContext sslContext = new JdkSslContext(SSLContext.getDefault(), true, ClientAuth.NONE);

return WebClient .builder() .clientConnector(new ReactorClientHttpConnector (HttpClient.create() .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)))) .filter(oauth) .baseUrl(appProperties.getServicesUrl()) .build();

Just 2 importants things to notice :

  1. Setting SSLContext.getDefault() is the only way to get all "Djavax.net.ssl.*****" values , and this SSLContext is from javax.net.ssl.SSLContext.
  2. Then you need to convert it to io.netty.handler.ssl.SslContext which is required by ReactorClientHttpConnector

You don't need to do any manual SSLContext config anymore. I removed them all and it's perfectly working fine !

I hope it helps