IIS URL Rewrites with web.config

IIS’ URL Rewrite module is a fast, powerful way to enable HTTP to HTTPS redirection on a website, publish HSTS headers and add useful functionality like redirects.

I use IIS URL rewrite and web.config to handle:

  1. Custom HTTP error pages
  2. HTTP to HTTPS redirects
  3. Get SSL certificates from Let’s Encrypt
  4. Publish HSTS headers
  5. Set up my own URL redirectors

Since URL Rewrite stores it’s configuration in web.config, I also use git to make configuration changes and deploy them to my webservers.

My basic web.config

Here’s a typical web.config file I start with.

Deployment is easy: install URL Rewrite on your webserver(s), drop the file in the root of the virtual host/site, and you should be good to go. In my case, I commit into my git repository, and it is deployed to my web servers.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="ACME" stopProcessing="true">
                    <match url="^\.well-known(.*)" />
                    <action type="None" />
                </rule>
                <rule name="HTTP to HTTPS redirect" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions>
                        <add input="{HTTPS}" pattern="off" ignoreCase="true" />
                    </conditions>
                    <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
                </rule>
                <rule name="Permanent Redirect 1" stopProcessing="true">
                    <match url="^ip$" />
                    <conditions />
                    <action type="Redirect" url="http://myip.dnsmadeeasy.com/" redirectType="Permanent" />
                </rule>
                <rule name="Redirect rule for rewrite map">
                    <match url=".*" />
                    <conditions>
                        <add input="{go:{REQUEST_URI}}" pattern="(.+)" />
                    </conditions>
                    <action type="Redirect" url="{C:1}" appendQueryString="false" />
                </rule>
                <rule name="zzzcatchall" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions />
                    <action type="Redirect" url="https://justinho.com/" redirectType="Temporary" />
                </rule>
            </rules>
            <outboundRules>
                <rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
                    <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
                    <conditions>
                        <add input="{HTTPS}" pattern="on" ignoreCase="true" />
                    </conditions>
                    <action type="Rewrite" value="max-age=31536000; includeSubDomains; preload" />
                </rule>
            </outboundRules>
            <rewriteMaps>
                <rewriteMap name="go">
                    <add key="/dns" value="https://cp.dnsmadeeasy.com/u/82918" />
                </rewriteMap>
            </rewriteMaps>
        </rewrite>
        <httpErrors>
            <remove statusCode="404" subStatusCode="-1" />
            <error statusCode="404" prefixLanguageFilePath="" path="/404.html" responseMode="ExecuteURL" />
        </httpErrors>
    </system.webServer>
</configuration>

Custom 404 error page

Any default error pages are unfortunate, and IIS’ default 404 page is no exception. So create your own – I named mine 404.html, creatively – and tell IIS to send 404 errors there:

        <httpErrors>
            <remove statusCode="404" subStatusCode="-1" />
            <error statusCode="404" prefixLanguageFilePath="" path="/404.html" responseMode="ExecuteURL" />
        </httpErrors>

HTTP to HTTPS redirection

Websites should always be accessible via HTTPS. And any access to http should be redirected to https. Enough said.

              <rules>
                <rule name="HTTP to HTTPS redirect" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions>
                        <add input="{HTTPS}" pattern="off" ignoreCase="true" />
                    </conditions>
                    <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
                </rule>
              </rules>

Except .well-known for Let’s Encrypt (sometimes)

The above rule alone will break Let’s Encrypt’s ability to get certificates, since the request to http://.../.well-known/* will be redirected to https.

So as part of my certificate renewal process, this stanza is temporarily enabled, ordered so it is the first rule to execute:

              <rules>
                <rule name="ACME" stopProcessing="true">
                    <match url="^\.well-known(.*)" />
                    <action type="None" />
                </rule>
              </rules>

Enable Strict Transport Security

With this rule, I force HTTPS on all subdomains of a particular domain. This is configured to only enable when on HTTPS (i.e. only tell clients to be secure when that message itself is delivered securely) per the RFC. I also mark it for preload, so that I can add the domain to the HSTS preload list.

            <outboundRules>
                <rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
                    <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
                    <conditions>
                        <add input="{HTTPS}" pattern="on" ignoreCase="true" />
                    </conditions>
                    <action type="Rewrite" value="max-age=31536000; includeSubDomains; preload" />
                </rule>
            </outboundRules>

Add redirects to other sites

Next, I have some URLs that I want easily accessible, so I set up fast HTTP redirects so I can share the links with others.

My example here has 2 types of redirects:

  1. ones that are permanent and support some regular expression targeting,
  2. and ones that blindly redirect to a specific URL while dropping the query string.

Permanent redirect, with regular expression targeting

Note that match url here allows you to put more complex regular expressions in place, then pass those matched groups to the destination, if you desire.

                <rule name="Permanent Redirect 1" stopProcessing="true">
                    <match url="^ip$" />
                    <conditions />
                    <action type="Redirect" url="http://myip.dnsmadeeasy.com/" redirectType="Permanent" />
                </rule>

Regular redirect

I prefer the temporary redirect, since it allows me to update the redirect URL and not have clients caching the old URL. Also I’m doing this with a rewrite map, so I can quickly add hundreds of redirects into this map, from a web.config file, with no database or backend required.

Hook the map to match all requests first:

                <rule name="Redirect rule for rewrite map">
                    <match url=".*" />
                    <conditions>
                        <add input="{go:{REQUEST_URI}}" pattern="(.+)" />
                    </conditions>
                    <action type="Redirect" url="{C:1}" appendQueryString="false" />
                </rule>

Then populate the rewrite map.

This will redirect domain.com/dns to https://cp.dnsmadeeasy.com/u/82918

            <rewriteMaps>
                <rewriteMap name="go">
                    <add key="/dns" value="https://cp.dnsmadeeasy.com/u/82918" />
                </rewriteMap>
            </rewriteMaps>

Enjoy.