springspring-webfluxmutual-authentication

Spring Webflux : mutual authentication fails on second request


I am in a context of TLS mutual authentication (server certificate and client certificate).

My client is a Spring Boot application with webflux (WebClient). The WebClient instance is created once and for all (the client runs as a service 24/24h).

My server is also a Spring Boot application configured for mutual authentication.

The first request works like a charm.

The second request (about 10 minutes later) always fails with the following error on client side :

javax.net.ssl.SSLException: Received fatal alert: internal_error
        at sun.security.ssl.Alert.createSSLException(Alert.java:133) ~[?:?]
        at sun.security.ssl.Alert.createSSLException(Alert.java:117) ~[?:?]
        at sun.security.ssl.TransportContext.fatal(TransportContext.java:356) ~[?:?]
        at sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:293) ~[?:?]
        at sun.security.ssl.TransportContext.dispatch(TransportContext.java:202) ~[?:?]
        at sun.security.ssl.SSLTransport.decode(SSLTransport.java:171) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:736) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:691) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:506) ~[?:?]
        at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:482) ~[?:?]
        at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:679) ~[?:?]
        at io.netty.handler.ssl.SslHandler$SslEngineType$3.unwrap(SslHandler.java:297) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1352) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1245) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1294) ~[netty-handler-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) ~[netty-codec-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468) ~[netty-codec-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[netty-codec-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800) ~[netty-transport-classes-epoll-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:499) ~[netty-transport-classes-epoll-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:397) ~[netty-transport-classes-epoll-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.89.Final.jar!/:4.1.89.Final]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.89.Final.jar!/:4.1.89.Final]
        at java.lang.Thread.run(Thread.java:831) [?:?]

... and the following error on server side :

org.apache.tomcat.util.net.TLSClientHelloExtractor The ClientHello was not presented in a single TLS record so no SNI information could be extracted

I suppose the server expects the client to authenticate again and I suppose the client does not feel the need to authenticate again.

Do I need to configure further my WebClient in order to maintain the connection open or something like that ?

I could recreate the WebClient for every request but I think it's not necessar with the proper configuration.

Thanks for helping.


Solution

  • I ended up fixing this issue.

    This was the faulty code for WebClient instanciation:

    var keyStoreInputStream = keyStoreResource.getInputStream();
    var keyStore = readKeyStore(keyStoreType, keyStoreInputStream, keyStorePassword);
    var keyManagerFactory = KeyManagerFactory.getInstance(getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, keyStorePassword);
    
    var trustStoreInputStream = trustStoreResource.getInputStream();
    var trustStore = readKeyStore(trustStoreType, trustStoreInputStream, trustStorePassword);
    var trustManagerFactory = TrustManagerFactory.getInstance(getDefaultAlgorithm());
    trustManagerFactory.init(trustStore);
    
    var sslContext = forClient().
            keyManager(keyManagerFactory).
            trustManager(trustManagerFactory).
            build();
    
    var httpClient = HttpClient.create().
            secure(ssl -> ssl.sslContext(sslContext));
    
    var clientConnector = new ReactorClientHttpConnector(httpClient);
    
    var webClient = WebClient.builder().
            clientConnector(clientConnector).
            baseUrl(baseUrl).
            build();
    

    With this configuration, the underlying channel is closed after 60 seconds of inactivity (without requests). I don't know if it's due to the client or server configuration.

    Once the channel is closed, if I do another request, it automatically opens a new channel but it reuses the same SSL session. That's why the client does not perform a new SSL handshake. That's why I got the following error on server side:

    org.apache.tomcat.util.net.TLSClientHelloExtractor The ClientHello was not presented in a single TLS record so no SNI information could be extracted
    

    My idea to fix this issue was to make the SSL session expire BEFORE the channel does with this additional SSL context configuration:

    var sslContext = forClient().
            keyManager(keyManagerFactory).
            trustManager(trustManagerFactory).
            sessionTimeout(30).
            build();
    

    It happens to work, the SSL session times out before the channel does and the SSL handshake is performed again if necessary.

    I think it's not a very clean solution and I'm still open to suggestions but I consider this issue as fixed.