21

I have Apache 2.2 with mod_ssl and a bunch of sites in HTTPS on the same IP/port with VirtualHosting, so client must support SNI to connect to those virtual hosts.

I would like to configure my server the following way:

When a user types www.dummysite.com and his browser supports SNI (Server Name Indication), any HTTP request is redirected to https:// where an HSTS header is sent. But if the browser doesn't support SNI then the request is served by HTTP.

The above rule, stated as is, is actually a fallback rule for those people that still run old browsers, as Mozilla and Chrome don't have this problem, just to avoid leaving these users out of the site.

I would like to do this redirecting at the Apache config level, perhaps with a filter on the user agent. I wouldn't like to touch running applications except making sure that no direct http:// references are present (otherwise they imply a security warning)

[Edit] (while editing the question I forgot the question): what is the list of SNI-enabled user agents to redirect?

usr-local-ΕΨΗΕΛΩΝ
  • 2,339
  • 7
  • 33
  • 50

6 Answers6

20

Since SNI occurs during the SSL/TLS handshake, it's not possible to detect browser support when the client connects to HTTP.

So, you're right; a user-agent filter is the only way to do this.

The big question is whether you want to act on a blacklist against browsers that you know won't listen for SNI, or a whitelist of browsers that are known to support it. Obscure or new devices being unable to use the site seems like a deal-breaker, so I'd say the whitelist might be the better option.

In your HTTP <VirtualHost>:

# Internet Explorer 7, 8, 9, on Vista or newer
RewriteCond %{HTTP_USER_AGENT} MSIE\s7.*Windows\sNT\s6 [OR]
RewriteCond %{HTTP_USER_AGENT} MSIE\s8.*Windows\sNT\s6 [OR]
RewriteCond %{HTTP_USER_AGENT} MSIE\s9.*Windows\sNT\s6 [OR]
# Chrome on Windows, Mac, Linux
RewriteCond %{HTTP_USER_AGENT} Windows\sNT\s6.*Chrome [OR]
RewriteCond %{HTTP_USER_AGENT} Macintosh.*Chrome [OR]
RewriteCond %{HTTP_USER_AGENT} Linux.*Chrome [OR]
# Firefox - we'll just make the assumption that all versions in the wild support:
RewriteCond %{HTTP_USER_AGENT} Gecko.*Firefox
RewriteRule ^/(.*)$ https://ssl.hostname/$1 [R=301]

Here's the blacklist option, too - keep in mind that this runs the risk of sending a client that doesn't use SNI to an SNI-needed site, but on the other hand, will send users of something new like IE 10 to the right place:

# IE 6
RewriteCond %{HTTP_USER_AGENT} !MSIE\s6
# Windows XP/2003
RewriteCond %{HTTP_USER_AGENT} !Windows\sNT\s5
# etc etc
RewriteRule ^/(.*)$ https://ssl.hostname/$1 [R=301]

There are a lot of browsers out there. I've been pretty loose with the expressions and haven't covered a lot of browsers - this could turn into quite the nightmare to maintain.

Whichever option you choose.. good luck!

Shane Madden
  • 112,982
  • 12
  • 174
  • 248
  • 1
    Great! I tested with Opera Mobile (which is SNI compliant but not in the list) and it doesn't redirect. With my Firefox, it does redirect! – usr-local-ΕΨΗΕΛΩΝ May 16 '12 at 21:10
  • 6
    I suggest modularizing this solution with the BrowserMatch directive from mod_setenvif http://httpd.apache.org/docs/2.2/mod/mod_setenvif.html . Use BrowserMatch to set an environment variable supports_sni=y, then write RewriteCond %{ENV:supports_sni} =y. That way, you can reuse the SNI detection logic for any other RewriteRules that you might have. – 200_success May 16 '12 at 22:58
  • @200_success Good idea! – Shane Madden May 17 '12 at 00:02
9

My solution is this:

  # Test if SNI will work and if not redirect to too old browser page
  RewriteCond %{HTTPS} on
  RewriteCond %{SSL:SSL_TLS_SNI} =""
  RewriteRule ^ http://www.example.com/too-old-browser [L,R=307]

If an old browser without SNI tries to access https://www.example.com/* then it will throw a error on the browser first, which cannot be avoided since until apache replies to a non-SNI browser it doesn't know which site it is asking for. Then it redirects to a page telling the user their browser is too old (as long as user clicks continue to website).

And for users with new browsers I have

  #Test if new browser and if so redirect to https
  #new browser is not MSIE 5-8, not Android 0-3
  RewriteCond %{HTTPS} off
  RewriteCond %{HTTP_USER_AGENT} !MSIE\ [5-8]
  RewriteCond %{HTTP_USER_AGENT} !Android.*(Mobile)?\ [0-3]
  RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

That excludes most of the old browsers, including some like MSIE 5-8 on Vista (9+ is only Vista/7 so supports SNI). It's not 100% (symbian is ignored etc.) but should work for the majority. The minority can still choose to accept the certificate error.

malc_b
  • 91
  • 1
  • 2
3

As far as I'm aware there's not really a good way to do this -- You can use a mod_rewrite rule or similar conditionally based on the User-agent header, but it would have to be on a NON-SSL vhost: If the browser does not support SNI and it goes to a secure (https://) site it's going to get the old-school Apache behavior of "Here's the first SSL certificate I have associated with that IP address -- Hope it's what you wanted!" -- If that's not the certificate the browser was expecting you'll wind up with an error message about hostname mismatches.

This basically means people have to hit a non-SSL interstitial page that will redirect them - possibly exposing any data they're sending in their request. This may or may not be a deal breaker (you say you're going to send them to a non-SSL site anyway if they don't support SNI, so I assume you don't care that much about security. If I were designing a system that had a need for SSL as an encryption or authentication layer I'd be a bit more insistent about it though...)

None of that stops someone from bookmarking the secure site though - and if they use a shared bookmark service or restore their bookmarks to a machine where the web browser doesn't support SNI they're back in the Potential-for-SSL-Errors case.

voretaq7
  • 79,345
  • 17
  • 128
  • 213
1

I'd be tempted to solve this one of three ways:

  1. RewriteRule based on User-Agent headers.
  2. Load a https:// URI in a <SCRIPT> tag on a non-default VHost; if the load succeeds, it's a bit of JS that reloads the whole page under HTTPS.
  3. Teach my visitors to use something like HTTPS Everywhere if this is a priority for them, force HTTPS on pages where it should be required, and hope everything works out in the end.

Of these, I personally like #2 the best, but that involves modifying your sites' code.

BMDan
  • 7,129
  • 2
  • 22
  • 34
0

Just for anyone needing it.

If you have multiple hosts and want all them to be SSL-enabled in VirtualHosting (and you bought a certificate for each) try the new mod_djechelon_ssl

$ cat /etc/apache2/mod_djechelon_ssl.conf 
RewriteEngine on
# Internet Explorer 7, 8, 9, on Vista or newer
RewriteCond %{HTTP_USER_AGENT} MSIE\s7.*Windows\sNT\s6 [OR]
RewriteCond %{HTTP_USER_AGENT} MSIE\s8.*Windows\sNT\s6 [OR]
RewriteCond %{HTTP_USER_AGENT} MSIE\s9.*Windows\sNT\s6 [OR]
# Chrome on Windows, Mac, Linux
RewriteCond %{HTTP_USER_AGENT} Windows\sNT\s6.*Chrome [OR]
RewriteCond %{HTTP_USER_AGENT} Macintosh.*Chrome [OR]
RewriteCond %{HTTP_USER_AGENT} Linux.*Chrome [OR]
# Firefox - we'll just make the assumption that all versions in the wild support:
RewriteCond %{HTTP_USER_AGENT} Gecko.*Firefox [OR]
#Safari iThing
RewriteCond %{HTTP_USER_AGENT} Mozilla.*iPhone.*Safari [OR]
RewriteCond %{HTTP_USER_AGENT} Mozilla.*iPod.*Safari [OR]
RewriteCond %{HTTP_USER_AGENT} Mozilla.*iPad.*Safari [OR]
RewriteRule ^/(.*)$ https://%{HTTP_HOST}/$1 [R=permanent,L]

Usage:

<VirtualHost ip:80>
ServerName www.yourhost.com

Include /path/to/mod_djechelon_ssl.conf

[plain old Apache directives]
</VirtualHost>

<VirtualHost ip:443>
ServerName www.yourhost.com

[SSL-related directives]

[Copy and paste directives from above host]
</VirtualHost>
usr-local-ΕΨΗΕΛΩΝ
  • 2,339
  • 7
  • 33
  • 50
0

As I posted here, you can only test for SNI support prior to requiring it. That is, you cannot force users onto SNI HTTPS and then fall-back if they don't support it, because they will receive an error like this (from Chrome on Windows XP) with no way to proceed.

So (unfortunately) the user has to actually begin over an insecure HTTP connection and then be upgraded only if they support SNI.

You can detect SNI support via:

  1. Remote script
    From your plain HTTP page, load a <script> from your destination SNI HTTPS server and if the script loads and runs correctly, you know the browser supports SNI.

  2. Cross-Domain AJAX (CORS)
    Similar to option 1, you could try performing a cross-domain AJAX request from the HTTP page to the HTTPS, but be aware that CORS has only limited browser support.

  3. Sniff the user-agent
    This is probably the least reliable method, and you will need to decide between having a blacklist of browsers (and operating systems) known not to support it, or a whitelist of known systems that do.

    We know that all versions of IE, Chrome & Opera on Windows XP and below do not support SNI. See CanIUse.com for full list of supported browsers.

Simon East
  • 1,484
  • 1
  • 14
  • 18