by

Disable HSTS on Dokku

HTTP Strict Transport Security, or in short HSTS, is a mechanism that forces web browsers to use secure connections (HTTPS) in order to access websites. It is a good idea and websites should use it. However there are some very niche cases where you do not want it. I manage one of those cases.

I run http://referrer-policy.info, a website that allows you to test how your browser implements the Referrer-Policy header. It is a long story (see my article Notes on implementing a Referrer Policy), but that website needs to be able to run both HTTP and HTTPS. If HSTS was active, it would disallow HTTP connections, and the site would lose utility. Therefore it has to be disabled.

HSTS on Dokku

I host the website in question on a VPS running Dokku. Dokku is configured by default to use HSTS, which again is a great idea in general, but it is not good for my special case.

Fortunately, this can be overriden. On Dokku, disabling HSTS involves two steps:

  1. Disabling the HSTS header.
  2. Removing redirects to HTTPS.

Do you need HTTPS in the first place?

The answer to this question is normally yes, but if that was your case you probably would not be reading this article.

If your site can run using HTTP only, simply do not enable HTTPS for your app, and Dokku will not force you to use it. HSTS will not be a problem here.

But if you do want your website to work both over HTTP and HTTPS, then you need to disable HSTS explicitly, which I will cover next.

Disabling the HSTS header

If you search for HSTS through the documentation, you will find a mention on the page about the Nginx Proxy.

Before following those instructions, let's first check that the HSTS header is indeed there, using curl:

 1$ curl -i http://my-site.example/
 2HTTP/1.1 301 Moved Permanently
 3Server: nginx
 4Date: Thu, 20 Oct 2022 13:14:41 GMT
 5Content-Type: text/html
 6Content-Length: 162
 7Connection: keep-alive
 8Location: https://my-site.example:443/
 9Strict-Transport-Security: max-age=15724800; includeSubdomains
10
11<html>
12<head><title>301 Moved Permanently</title></head>
13<body>
14<center><h1>301 Moved Permanently</h1></center>
15<hr><center>nginx</center>
16</body>
17</html>

Yes, it is there. The header Strict-Transport-Security: max-age=15724800; includeSubdomains is the one. Let's remove it now:

1$ dokku nginx:set my-app hsts-max-age 0
2$ dokku proxy:build-config my-app

Note that I am setting hsts-max-age 0 instead of simply hsts false. This means that the HSTS header will still be there, but it will show a max-age of 0. This tells browsers that the HSTS setting has expired and they should stop fulfilling it. By doing it this way, we get to "reeducate" browsers that accidentally saw the HSTS header before it was removed. It is not a bullet-proof solution though, and it may remain in browser caches for a bit.

Do not forget running the command proxy:build-config. Until recently, it was a bit hidden in the documentation and I missed it the first time. Fortunately this has been corrected now.

But anyway, another hit with curl should show that the header now appears with a max-age=0 parameter:

 1$ curl -i http://my-site.example/
 2HTTP/1.1 301 Moved Permanently
 3Server: nginx
 4Date: Thu, 20 Oct 2022 13:14:41 GMT
 5Content-Type: text/html
 6Content-Length: 162
 7Connection: keep-alive
 8Location: https://my-site.example:443/
 9Strict-Transport-Security: max-age=0; includeSubdomains
10
11<html>
12<head><title>301 Moved Permanently</title></head>
13<body>
14<center><h1>301 Moved Permanently</h1></center>
15<hr><center>nginx</center>
16</body>
17</html>

But if you visit the app with your browser, you will still land on the HTTPS version. What gives?

A note about HSTS and redirects

When the browser sees the HSTS header, it makes a note to not use the HTTP site in the future. Any time you type an HTTP URL for the site, or you follow a link to it, the browser automatically will replace http:// with https://, without ever hitting the HTTP site again. It is a client-side measure enforced by the browser.

A detail of the HSTS standard is that the header is only valid if served over HTTPS. If the browser request was made with HTTP, the header must be ignored (long story, but it is a safety feature). This means that for HSTS to work, we should make sure to redirect anyone who visits the HTTP site, and send them to the HTTPS version. Once working over HTTPS, the browser will see the header and enforce it.

So this redirect is what we need to remove. If you have a look at the curl outputs above, you will see that the HTTP status is 301 Moved Permanently, with the header Location: https://my-site.example:443/. Dokku serves this by default when HTTPS is enabled.

Removing the redirect to HTTPS

Removing this is a bit trickier. At the moment Dokku does not offer a simple configuration option to remove this redirect, but it does offer access to the Nginx configuration of individual apps, and this is all we need.

On the same page that the Dokku documentation describes HSTS, there is a section on Customizing the nginx configuration. It describes how each Dokku app uses an individual Nginx configuration, created from a template that can be found at their repo. This template can be downloaded, added to our own apps, and altered to suit our needs. That is what we will do.

Start by downloading the template into your project's repo:

1$ curl -O https://raw.githubusercontent.com/dokku/dokku/master/plugins/nginx-vhosts/templates/nginx.conf.sigil

(Note that the location of the template may change in future versions. Check the documentation if that happens).

Now you have a nginx.conf.sigil file on your project. It must be on the root directory of the repo, and it will be picked up by Dokku on deployment, replacing the default configuration.

From this file, you need to remove the parts about the redirect. Personally, I prefer to comment them out for easier future reference. The following is a diff of the changes, as extracted from my project:

 1diff --git a/nginx.conf.sigil b/nginx.conf.sigil
 2index 21a0d01..2a76711 100644
 3--- a/nginx.conf.sigil
 4+++ b/nginx.conf.sigil
 5@@ -11,12 +11,24 @@ server {
 6   {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }}
 7   access_log  {{ $.NGINX_ACCESS_LOG_PATH }}{{ if and ($.NGINX_ACCESS_LOG_FORMAT) (ne $.NGINX_ACCESS_LOG_PATH "off") }} {{ $.NGINX_ACCESS_LOG_FORMAT }}{{ end }};
 8   error_log   {{ $.NGINX_ERROR_LOG_PATH }};
 9+
10+{{/*
11+##### <custom> #####
12+##
13+## Conditional commented out for my-site.example, as we do
14+## want to allow both HTTP and HTTPS domains.
15+##
16+## Also remember to disable HSTS on Dokku's own config as
17+## described at https://dokku.com/docs/networking/proxies/nginx/#hsts-header
18+##
19+##### </custom> #####
20 {{ if (and (eq $listen_port "80") ($.SSL_INUSE)) }}
21   include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
22   location / {
23     return 301 https://$host:{{ $.PROXY_SSL_PORT }}$request_uri;
24   }
25 {{ else }}
26+*/}}
27   location    / {
28
29     gzip on;
30@@ -63,7 +75,7 @@ server {
31     internal;
32   }
33   include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
34-{{ end }}
35+{{/* end */}}
36 }
37 {{ else if eq $scheme "https"}}
38 server {

If you apply the outlined changes and deploy, the redirect will be gone. Now you should be able to visit the HTTP and HTTPS versions of the app without issue. This may still require some browser caches to expire though, as they may still remember the redirect and apply it, without actually visiting the page.

Caveats!

There are two caveats that I have found, where things will not work as expected and your browser will still send you to the HTTPS site. They are not really specific to Dokku, but instead apply to HSTS in general.

The first is with Firefox in Private Browsing mode (aka. incognito mode). Since version 91, Firefox forces HTTPS in Private Browsing mode. Only if the site does not support HTTPS at all, it will allow using HTTP. But if HTTPS is available, users will be sent there regardless of any configuration you attempt.

The second is with .dev domains. Both Chrome and Firefox force HSTS on .dev domains by default. There is nothing that you can do to prevent this. Just know that you will have to use a different TLD for your app if you need to disable HSTS.