Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
ealexhaywood opened this issue Mar 12, 2019 · 17 comments
Closed

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

ealexhaywood opened this issue Mar 12, 2019 · 17 comments
Labels
for/springframework This belongs to the Spring Framework project for/stackoverflow Questions are best asked on SO or Gitter

Comments

@ealexhaywood
Copy link

ealexhaywood commented Mar 12, 2019

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
Copy link
Member

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

@violetagg violetagg added the for/stackoverflow Questions are best asked on SO or Gitter label Mar 13, 2019
@ealexhaywood
Copy link
Author

ealexhaywood commented Mar 13, 2019

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()

@violetagg
Copy link
Member

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
Copy link
Author

ealexhaywood commented Mar 13, 2019

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
Copy link
Member

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

@violetagg violetagg added the for/springframework This belongs to the Spring Framework project label Mar 19, 2019
@bclozel
Copy link
Member

bclozel commented Mar 20, 2019

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
Copy link
Author

ealexhaywood commented Apr 1, 2019

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
Copy link
Member

@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
Copy link
Author

@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
Copy link
Member

@ealexhaywood JVM options

@ealexhaywood
Copy link
Author

Right, which is different from setting the _JAVA_OPTIONS variable.

@voodemsanthosh
Copy link

voodemsanthosh commented May 20, 2020

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
Copy link

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
Copy link

SkotnikovD commented Jun 23, 2020

@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
Copy link

mplain commented Sep 20, 2020

@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
Copy link

rit0m commented Nov 9, 2023

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for/springframework This belongs to the Spring Framework project for/stackoverflow Questions are best asked on SO or Gitter
Projects
None yet
Development

No branches or pull requests

8 participants