Ian J. MacIntosh

Fixing Surprising HSTS Behavior from Cloudflare and Netlify

Mozilla Observatory said my website was missing HTTP Strict-Transport-Security (HSTS) headers, which surprised me since I went out of my way to add them to my Netlify server configuration (netlify.toml). Eventually I figured out my CDN (Cloudflare) quietly modifies the response headers that get passed from my origin server to the visitor.

If you’re running into the same problem:

  1. Open your site’s Cloudflare dashboard
  2. SSL/TLS -> Edge Certificates
  3. Click “Change HSTS Settings
  4. Check the box that says “I Understand” and click “Next
  5. Toggle “Enable HSTS (Strict-Transport-Security)
  6. Set “Max Age Header (max-age)” to “12 months
  7. Click “Save

Now your site will set a restrictive HSTS policy for visitors.

Those are the “grip it and rip it” instructions, where I glossed over a couple of things:

How I Figured All That Out

My troubleshooting journey for this issue had a couple of plot twists, which I’ll explain in case someone else finds themselves on the same path.

My website is hosted on Netlify, with Cloudflare’s CDN in front of it. My netlify.toml file has a line in it: Strict-Transport-Security = "max-age=63072000"

Running curl to get the HTTP headers for ianjmacintosh.com showed strict-transport-security: max-age=0

Weird. It’s not just like the header is missing (which I’d expect if I’d made a simple syntax error in the TOML file or referred to the wrong property), but it’s displaying a totally different value which I’ve never given it. At this point, it wasn’t clear to me if Netlify was changing stuff or Cloudflare, since both handle the request.

So I decided to remove Cloudflare from the equation and access my website directly from Netlify, using its netlify.app URL. Every Netlify site has one of these in addition to whatever custom domain you host from.

Running curl to get the HTTP headers from my netlify.app URL returned strict-transport-security: max-age=31536000; includeSubDomains; preload

That’s even weirder. It’s my app returning another completely different HSTS header that I’ve never specified anywhere. After some searching, I found Netlify appends this header to all requests handled through their netlify.app domains, as documented exclusively in this message board post: https://answers.netlify.com/t/security-headers-adding-includesubdomains-and-preload-to-strict-transport-security-header-to-sites-with-default-domain-name/19706

I decided to focus on Cloudflare to set my HSTS headers (maybe by setting a transform rule on the HTTP headers) but stumbled across the HSTS settings panel I pointed out earlier. After changing that, I got myself an HSTS header that made sense.

Good enough! Although I’d still like to learn how to configure Cloudflare to simply pass the values of my HSTS headers along from the origin server.

Extra Credit

I was unsatisfied with not knowing if my Netlify server actually used the configuration I gave it, so I spent a little extra time to request my website directly from Netlify using my ianjmacintosh.com domain.

First I got the IP address my app’s netlify.app domain points to (by running dig ianjmacintosh-burgeoning-wombat.netlify.app from my terminal) and added a new line in my /etc/hosts file to resolve ianjmacintosh.com to that IP address. The IP address dig gave me is an IPv4 address, so I’ll need to be sure to request my site using IPv4.

Next, I requested my site. I ran curl -4Is https://www.ianjmacintosh.com

The response confirmed that Netlify honored my settings; curl displayed strict-transport-security: max-age=63072000

But in the end, it’s a moot point. There’s no way to instruct Cloudflare to pass the header along as-is and allow the origin server to manage its own HSTS configuration.

Closing Thoughts

This whole journey impressed upon me how useful it is to maintain a side project. It also reminded me how important audit tools can be in the on-going effort to keep my knowledge up-to-date — if Mozilla Observatory hadn’t prompted me to use HSTS, I wouldn’t have learned about any of this. Most importantly, it’s way better to get a hands-on understanding about how Netlify and Cloudflare handle HSTS in a low stakes environment instead of grasping at straws in a Network Operations Center during an outage.