jetty.project
jetty.project copied to clipboard
Invalid SNI even when sniRequired=false
Jetty version(s) 12.0.7
Jetty Environment core
Java version/vendor (use: java -version)
openjdk version "21.0.1" 2023-10-17 LTS
OpenJDK Runtime Environment Zulu21.30+15-CA (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Zulu21.30+15-CA (build 21.0.1+12-LTS, mixed mode, sharing)
OS type/version Microsoft Windows [Version 10.0.19045.4046]
Description
https://eclipse.dev/jetty/documentation/jetty-12/operations-guide/index.html#og-protocols-ssl-sni states that when sniRequired=false
the SNI checks should always pass.
If you take a look at the attached DEBUG log, you'll notice that sniRequired=false
(the default setting. I didn't alter it) but the server still responds with org.eclipse.jetty.http.BadMessageException: 400: Invalid SNI
.
I suspect I know what is going wrong in my particular case: the browser is hitting the server by IP address, and that IP address happens to be my own IP address. I'm not sure if that ends up getting rerouted to localhost but it could be the case.
Per https://stackoverflow.com/a/69945374/14731:
Know that SNI itself has restrictions:
- localhost is not allowed
- IP Address Literals are not allowed (eg: 192.168.1.215 or fe80::3831:400c:dc07:7c40)
If the failure I am seeing is expected behavior (working as designed) then I'd suggest making the following changes:
- Update the documentation to mention these SNI restrictions.
- Add more information to the exception message, such as
Invalid SNI. localhost is not allowed
orInvalid SNI IP address literals are not allowed
, etc.
Out of curiosity, is there any way to bypass these restrictions? Or do I have to hit the server by name instead of by IP?
More clues...
- The server is configured with an SSL certificate for a domain called
licensed.app
. - Instead of hitting the server through that domain, I tried hitting it through a DDNS provider... so I was hitting the server by hostname instead of by IP address, I still got the same Invalid SNI exception.
I eventually redirected pointing licensed.app
to my local PC and hit the server through it. This time, the request was accepted without an error.
So it seems that the only way to hit the server is through the hostname that matches the SSL certificate.
Okay, I feel like an idiot :) My code contains new SecureRequestCustomizer()
. While it's true that sniRequired=false
, this constructor sets sniHostCheck=true
which is why I was getting this problem. Setting this property to false allowed me to hit the server by IP address and the DDNS provider.
I still suggest improving ease of troubleshooting for this case. The DEBUG logs don't seem to contain any hint that this is what went wrong. sniHostCheck
isn't mentioned at all.
2. Add more information to the exception message, such as
Invalid SNI. localhost is not allowed
orInvalid SNI IP address literals are not allowed
, etc.
This is the code that does this check ...
https://github.com/jetty/jetty.project/blob/3d3682597fac104504ece1b036b03b162d97c3b0/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java#L212-L231
When you run your DEBUG logs with your failing scenario do you get the DEBUG log output seen on line 223? If so, what does it say?
Having the BadMessageException say something different and specific can be viewed as a security issue. Instead, having meaningful DEBUG logging is a more likely solution.
@joakime
Having the BadMessageException say something different and specific can be viewed as a security issue. Instead, having meaningful DEBUG logging is a more likely solution.
Got it. Here is where I'm at now:
- I updated the code to require both
sniRequired=true
andsniHostCheck=true
. - I generated and installed an SSL certificate for licensed.app
- I configured the DNS to redirect requests from that domain to my home computer
- When I hit licensed.app with my browser I get the following DEBUG output:
DEBUG o.e.j.server.SecureRequestCustomizer.checkSni() - Host=licensed.app, SNI=null, SNI Certificate=X509@32673d16(null,h=[licensed.app, www.licensed.app],a=[],w=[])
My understanding is that this means that the client is not sending a SNI hostname as part of the request, but that doesn't make sense since all browsers are supposed to support this by now. Here are some clues that might help:
- The domain is hosted behind Cloudflare with
proxy=true
on their end. - I am using an abnormal port. I am hitting
https://licensed.app:8443/
though I doubt that is a problem. - My browser is Brave
[Version 1.62.165 Chromium: 121.0.6167.184 (Official Build) (64-bit)](https://brave.com/latest/)
which should be equivalent to the latest stable version of Chrome.
The documentation mentions the SNI=null
scenario, but it doesn't explain when that could happen. In other words, I don't know how to debug this further. Any ideas?
@cowwoc remember that DEBUG log statements are typically for Jetty developers to have a clearer understanding of what's going on.
In that particular log statement SNI=null
means that there was no SNI in the SSLSession
, from there it is supposed to be retrieved.
As a side note, remember that the SNI is sent per connection, not per request.
It really seems from your logs that the SNI is not sent.
Can you capture the network trace via tcpdump
or wireshark
?
Also, you are using Conscrypt
. Does it work if you use the JDK TLS implementation?
It might be a Conscrypt
bug, see for example: https://groups.google.com/g/conscrypt/c/vHXDvjssGZI (not sure how up-to-date that is though).
@sbordet Sorry for the delayed response. Yes, it seems to be a bug in Conscrypt. If I replace:
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>
<version>${jetty.version}</version>
<exclusions>
<exclusion>
<!-- Replaced by OS-specific artifact -->
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-client</artifactId>
<version>${jetty.version}</version>
<exclusions>
<exclusion>
<!-- Replaced by OS-specific artifact -->
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk</artifactId>
<version>${conscrypt.version}</version>
<classifier>${os.detected.classifier}</classifier>
</dependency>
with:
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-java-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-java-client</artifactId>
<version>${jetty.version}</version>
</dependency>
and remove:
Security.addProvider(new OpenSSLProvider());
sslContextFactory.setProvider("Conscrypt");
from my code, then the SNI error goes away and the debug log show the correct SNI value.
Can you please confirm that I was using Conscrypt correctly? Is it sufficient to include the dependencies I mentioned and invoking the two lines to configure sslContextFactory
to use it?
I tried removing the exclusions
and using the uber-jar version of Conscrypt that Jetty links to, but I got the same "Invalid SNI" error.
I'm digging further into this and it smells of user error. SslContextFactory
picks up the correct SNI but when SecureRequestCustomizer
invokes String sniHost = (String)session.getValue(SslContextFactory.Server.SNI_HOST);
it gets back null
. This seems to imply that I am not doing something to cause SslContextFactory
to pass the value to SecureRequestCustomizer
.
Any ideas?
The SNI host value is put into the SSLSession
by SniX509ExtendedKeyManager
, which is automatically configured if you have a certificate with multiple SAN, which is your case.
Your logs shows that SniX509ExtendedKeyManager
is in use, so all good on your part.
I really think that Conscrypt
does not support SNI on the server, which I think it's confirmed by the fact that if you switch to OpenJDK all works as it should.
I think you're right, but then... what did https://github.com/google/conscrypt/pull/712 add? I thought it was adding SNI support on the server.
Okay, so what are the next steps here?
Will you guys pick up the torch and follow up with the Conscrypt team? If so, https://github.com/google/conscrypt/issues/644 might be a good place to start.
I guess another thing to do is add an automated test for this scenario.
In light of the current restriction, what is the recommended behavior for production? Use Conscript with SNI disabled or use the slower Java implementation? It's not clear how much of a performance difference this would make, nor how big a security risk it would be to disable SNI.
@sbordet friendly bump