Blog

How to configure HSTS on www and other subdomains

It is very common to use the www subdomain as the primary website location. My own website www.danielmorell.com does just that.

Unfortunately, this causes HSTS implementation to be more complicated. However, I will be happy to guide you through the process.

Let's start with the basics...

What is HSTS?

HSTS stands for HTTP Strict Transport Security. HSTS was created after Moxie Marlinspike discovered SSL-stripping in 2009. It is a technique that silently downgrades an HTTPS connection to HTTP. HSTS is a way for websites to tell browsers that the connection should only ever be encrypted. HSTS was officially finalized by the Internet Engineering Task Force with RFC 6796 in late 2012.

The difficulty with HSTS from a security standpoint is that a browser needs to communicate with a website before it knows the website uses HSTS.

This is the digital equivalent of calling your spouse on a phone that has been wiretapped to tell them a bank account number, but to make it “secure” you start by telling them you are going to add 2 to each number. Sure, the bank account number was “encrypted” but you gave away the encryption.

The solution is to start with a secure connection. This requires that browsers know before it ever visits the website that it needs to connect with HTTPS. To accomplish this the Chromium team maintains the HSTS preload list. This preload list is shipped with each installation of Chrome and is also included in other web browsers like Edge, Safari, and Firefox. We will talk more about this later.

Basic HSTS implementation

HSTS is set by the webserver by sending the strict-transport-security response header to the browser. It looks like this.

Strict-Transport-Security: max-age=63072000

max-age is the length of time the browser should only use HTTPS to communicate with the domain in seconds. 6307200 equals two years.

This header tells the browser that anytime it attempts to connect to this domain it should do so via an encrypted connection. Your browser will not request an insecure resource or send an unsecured message to the server. It will first change to the encrypted protocol.

Practically, this means that browsers will internally redirect requests to HTTPS prior to sending the request to the server. Some browsers like Edge simply change the URL and move on, others like Chrome use a 307 internal redirect.

How browsers match Known HSTS Hosts

A Known HSTS Host is a domain that the browser knows implements HSTS. Prior to sending a request to the domain, a browser will check the domain against its Known HSTS Hosts. If there is a matching Known HSTS Host the request will be encrypted before it is sent.

Diagram showing HSTS terms identifying domain labels, superdomain, and subdomains.

A browser may identify a requested domain name as a match if it matches any superdomain match with an asserted includeSubDomain directive. A superdomain is a domain name to the right of a domain label. If danielmorell.com is a Known HSTS Host that uses the includeSubDomain directive, the subdomain www.danielmorell.com would result in a superdomain match.

If the superdomain matching method fails, a browser may identify a requested domain name as a match if a congruent match is found. A congruent match does not require the includeSubDomain directive. Thus www.danielmorell.com would be a congruent match of www.danielmorell.com.

Lastly, if no superdomain or congruent match can be made the browser will determine the requested domain is not a Known HSTS Host.

The HSTS preload list eligibility

The reason that we have discussed how HSTS and Known HSTS Host matching works is because it impacts the ability of your website to be eligible for the HSTS preload list that we mentioned earlier.

The HSTS preload list is a list of root domains that comply with the HSTS standard and have opted-in to be preloaded into the browser’s Known HSTS Host list.

To meet the HSTS preload list standard a root domain needs to return a strict-transport-security header that includes both the includeSubDomains and preload directives and has a minimum max-age of one year. Your site must also serve a valid SSL certificate on the root domain and all subdomains, as well as redirect all HTTP requests to HTTPS on the same host.

The impact on redirects

To maintain the security of your website and be eligible for the HSTS preload list you have two primary options. First, you can create a two-stage redirect for all HTTP traffic. Second, your primary site can be located on the root domain.

If we chose the first option we will need to create two redirect methods. The first will move traffic from non-www and HTTP to HTTPS and then to the www subdomain. The second will create a redirect if either the request is non-www or HTTP, but not both.

The reason we create the redirects like this is it allows us to server the HSTS header with the includeSubDomains directive on the root domain. The HSTS preload list only includes root domains. www.danielmorell.com is not eligible for the preload list since it is a subdomain.

By using only root domains and requiring the includeSubDomains directive the Chromium team can keep the preload list as short as possible.

The subdomain www.danielmorell.com can be matched to a Known HSTS Host using a superdomain match if danielmorell.com is in the preload list.

How to set the redirect and HSTS headers

There are three distinct possibilities that need to be accounted for when building our redirect and HSTS headers. For the sake of simplicity, I have created tables showing the response header progression for each possibility.

Responce headers for non-www and HTTP requests

Requested URL Responce Headers
http://example.com
Status Code: 301
Location: https://example.com
https://example.com
Status Code: 301
Location: https://www.example.com
strict-transport-security: max-age=63072000; includeSubdomains; preload
https://www.example.com
Status Code: 200
strict-transport-security: max-age=63072000; includeSubdomains; preload

Responce headers for www and HTTP requests

Requested URL Responce Headers
http://www.example.com
Status Code: 301
Location: https://www.example.com
https://www.example.com
Status Code: 200
strict-transport-security: max-age=63072000; includeSubdomains; preload

Responce headers for non-www and HTTPS requests

Requested URL Responce Headers
https://example.com
Status Code: 301
Location: https://www.example.com
strict-transport-security: max-age=63072000; includeSubdomains; preload
https://www.example.com
Status Code: 200
strict-transport-security: max-age=63072000; includeSubdomains; preload

How to set the headers with .htaccess

I will be showing how to implement this on an Apache server using a .htaccess file.

The first thing that needs to be done is to set the HSTS header on all HTTPS responses. There are a few differences between setting the HSTS header and adding most other headers.

  • First, you will need to include the always condition. This ensures that the header will be set on server responses, like redirects, that do not typically include custom headers.

  • Second, setting the HSTS header on an HTTP response is invalid. Therefore, we will want to avoid it and only serve the HSTS header over HTTPS.

It is common to check the enviroment using env=HTTPS to set the header on HTTPS responses. However, I have seen that this does not always play well with redirects. I recommend using an if statement to check the scheme prior to sending the response over the wire.

This can be done by simply wrapping the header in this if statement.

<if "%{HTTPS} == 'on'">
  ## place header here
</if>

After you have restricted the header to HTTPS responses you can build your HSTS header.

To be eligible for the HSTS Preload list you need to include both the includeSubDomains and the preload declaratives. The finished HSTS header code will look like this. Don’t forget to include the always condition.

<if "%{HTTPS} == 'on'">
  Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</if>

Warning:

Ensure your site, all subdomains, and all nested subdomains are working properly over HTTPS prior to setting the Strict-Transport-Security header! I recommend setting the max-age to something short when it is first set. max-age=300 five minutes is a good time period.

If you are working in a development environment, (I don’t recommend playing with HSTS on production) you can remove your test site from the Known HSTS Host list on Chrome here: chrome://net-internals/#hsts. This allows you to reset your HSTS and redirect tests.

Once you have set the HSTS header you can start building the redirects. The first redirect needs to move all HTTP traffic to HTTPS on the same host. This means that we cannot create a redirect from http://danielmorell.com to https://www.danielmorell.com, because danielmorell.com and www.danielmorell.com are different hosts. Once we have moved traffic from HTTP to HTTPS we can use a second redirect to move traffic to the www subdomain.

There are many ways to do this but this following method works well.

<if "%{HTTPS} == 'on'">
  Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</if>

## Since you have enabled HSTS the first redirection rule will
## instruct the browser to visit the HTTPS version of your site. 
## This prevents unsafe redirections through HTTP.

# Turn on Rewrite Engine
RewriteEngine On

# Redirect to secure HTTPS before changing host
RewriteCond %{HTTP_HOST} !^www\.(.*)$ [NC]
RewriteCond %{https} off [OR] 
RewriteCond %{HTTP:X-Forwarded-Proto} !https [OR]
RewriteCond %{HTTP:X-Forwarded-SSL} !https
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]

# Remove trailing slash from non-filepath urls
RewriteCond %{REQUEST_URI} /(.+)/$
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ https://www.example.com/%1 [R=301,L]

# Include trailing slash on directory 
RewriteCond %{REQUEST_URI} !(.+)/$
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^(.+)$ https://www.example.com/$1/ [R=301,L]

# Force HTTPS and WWW 
RewriteCond %{HTTP_HOST} !^www\.(.*)$ [OR,NC]
RewriteCond %{https} off [OR] 
RewriteCond %{HTTP:X-Forwarded-Proto} !https [OR]
RewriteCond %{HTTP:X-Forwarded-SSL} !https
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]

To custimize this code to work for your site you can use a .htaccess generator I created. If you do, be sure to select Yes for the Use HSTS preload compatible two-step redirect in the Advanced Setting.

Conclusion

If you use the www subdomain on your primary website, enabling HSTS is more complicated. However, that does not mean that it is beyond possible.

It simply takes the right header configuration and the correct redirect steps.

Daniel Morell

Daniel Morell

I am a fullstack web developer with a passion for clean code, efficient systems, tests, and most importantly making a difference for good. I am a perfectionist. That means I love all the nitty-gritty details.

I live in Wisconsin's Fox Valley with my beautiful wife Emily.

Daniel Morell

I am a fullstack web developer, SEO, and builder of things (mostly digital).

I started with just HTML and CSS, and now I mostly work with Python, PHP, JS, and Golang. The web has a lot of problems both technically and socially. I'm here fighting to make it a better place.

© 2018 Daniel Morell.
+ Daniel + = this website.