Cyotek has a number of different websites powering various bits of our software and services. Some are Windows based using IIS, and some are Linux based using Apache. Internal servers are are directly accessed but external servers are behind load balancers. Almost all are using HTTPS now and have redirects in place to force the use of HTTPS over HTTP.

There are plenty of articles on the internet dealing how to use .htaccess files to perform redirects using Apache, and various articles on different methods of redirecting IIS or ASP.NET applications. However, there seems to be a slight gap when it comes to load balancers or reverse proxies. Depending on how the load balancer / reverse proxy (referred to as just load balancer for the rest of the article) operates, the secure connection may terminate at the load balancer, and so the web server always receives "insecure" traffic.

If the secure connection is terminated at the balancer, then this can be a problem for the most common method of redirecting traffic, which is to check if the current request's scheme is HTTP and perform a redirect if it is. But if the web server always gets HTTP traffic, then this approach will simply result in an infinite redirect loop, something I've managed to do inadvertently more than once.

Fortunately however, when a load balancer forwards traffic to the final server, it generally includes some extra headers such as X-Forwarded-For and X-Forwarded-Proto. These headers provide details such as the original scheme/protocol being used and the origin IP address.

When researching the final details for writing this article, I found that the non-standard X-Forwarded-* headers have been replaced under RFC 7239 by a new Forwarded header. It's possible therefore that the X-Forwarded-For header may start dropping out of use and you'd be expected to work with Forwarded instead.

The header I'm interested in in this case is X-Forwarded-Proto which simply details the scheme of the original request. If the value is http, then clearly the original request wasn't secure and so I can issue a redirect. If it's https, then the original request - up to hitting the load balancer - was secure, and we shouldn't try and redirect, or we're back into infinite loop territory.

Remember that these are request headers, e.g. headers you can't control and which could be forged or manipulated.

When using IIS, I've mostly used code based redirects although the very oldest bits use an ancient configuration based re-writer named Intelligencia.UrlRewriter. However, for some time IIS now has had an optional URL rewriter of its own, which is what I'm going to use in this article.

By default the URL rewrite module isn't installed, although it's easy enough to add using the Web Platform Installer. I've detailed the steps in a prior post.

Adding a rule to force HTTPS

Although you can configure rules from within IIS Managers GUI interface, I'm just going to detail how to directly modify the web.config of an application.

In your web.config, find the system.webServer element (or add it as a child of configuration if it is not already present), and add a rewrite element containing the following content.

xml
<rewrite>
  <rules>
    <!-- Specify a rule element. The stopProcessing attribute means no other
    rules will be processed if this rule is a match. You can also add a boolean
    attribute named enabled which you can use to temporarily disable a rule
    without deleting it from the file -->
    <rule name="HTTPS Redirect" stopProcessing="true">
      <!-- Specifies a regular expression used to match the incoming URL. As I
      want all URL's to be considered, I tell it to match any character. By wrapping 
      the expression in brackets I create a capturing group I can use elsewhere. -->
      <match url="(.*)" />
      <!-- Each rule can have one or more conditions -->
      <conditions>
        <!-- Here I've added a condition which looks at the X-Forwarded-Proto
        header to see if it's any value other than "https". (The attribute
        negate="true" means "does not match") -->
        <add input="{HTTP_X_FORWARDED_PROTO}" pattern="https" negate="true" />
      </conditions>
      <!-- The action element states what should happen if all the rule
      conditions are met. This action instructs IIS to perform a 301 redirect
      to the original host (via the HTTP_HOST variable) with the original request
      path (as captured in the match element above) via R:1 -->
      <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" />
    </rule>
  </rules>
</rewrite>

Rather than trying to add a description outside the code block, I've added a bunch of comments inside the XML.

Happily, these are the only changes you need to make to web.config files - you don't need to define new configSections, httpHandlers, or other elements. I assume these have been pre-defined at a higher level, but haven't confirmed this.

A note on using LetsEncrypt renewal challenges

If you are renewing LetsEncrypt certificates via a HTTP challenge (where a file is placed on your server to confirm validity), then this rule will cause the challenge to fail due to the redirect. If your processes are set to only renew the certificates at the last minute then SSL can stop working on your website, causing web browsers to display unwelcome danger messages when visitors attempt to access your site. Somethign else that has happened more than once to me, and the main reason why cyotek.com is now running off a 3 year traditional certificate after lasts months mishap.

As the file is always in a static location (.well-known/acme-challenge) we can expand our original rule to add a new condition which prevents the redirect from triggering if that path is present.

Using the example rule above, I added a new child of the conditions element as follows.

xml
<add input="{PATH_INFO}" pattern="^/\.well-known/acme-challenge/.*" negate="true" />

When testing a HTML located in this path, the request was not forcibly redirected to HTTPS and was displayed via unsecure HTTP, which should allow LetsEncrypt challenges to succeed. Of course, the certificates on the domains where I've implemented these rules aren't due to expire for another two months so time will tell if this is sufficient.

Closing notes

Forcing a redirect from HTTP to HTTPS is only one part - you should also consider using a Content Security Policy (CSP) that instructs browsers to automatically use HTTPS and setting up HTTP Strict Transport Security (HSTS). You should also ensure the redirects you put in place are 301 to guide search engines to update references (the example above uses a 301 redirect). Lots of things to try and remember! Troy Hunt has a helpful post on getting started with HTTPS.

There is also lots more you can do using rewrite rules - you can view the documentation for more information and examples. And, although in this article I only covered modifying web.config directly, you can use the GUI tools in IIS Manager to explore all the supported function and experiment with rule creation.

Update History

  • 2017-11-19 - First published
  • 2020-11-22 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Comments

# Stefan Richter

Thank you for this, I've finally managed to solve a complicated setup behind an EC2 load balancer that uses an ancient version of IIS. I've never managed to redirect http to https on that machine without causing a redirect loop - the HTTP_X_FORWARDED_PROTO condition was the crucial piece to the puzzle. 👍

Reply

# Richard Moss

Stefan,

You're welcome, I'm glad this article was helpful for you!

Regards;
Richard Moss

Reply

# Damian

Hi Richard Any idea on how to fix "Mixed Content" issues with a very similar setup. I am only experiencing it on WEBP images though, very strange

Reply

# Richard Moss

Hello,

The use of a load balancer shouldn't affect this and it is more likely caused by how your HTML is generated. It sounds like you are serving pages with absolute URLs that point to http. Assuming these aren't from an external domain that you can't control, I have seen this when using MVC URL helpers to generate an absolute URL. Given your description of only experiencing these for WEBP images suggests you have a custom handler to support this format, and perhaps this isn't constructing URLs correctly. If this is the case, consider trying to use a relative URL, or by using //, e.g //example.com/image.webp and let the browser choose the appropriate protocol.

Regards;
Richard Moss

Reply