Liam

This website is also available over HTTPS.

Allowing for plaintext HTTP with Caddy

I was motivated by joshua stein's "Plaintext HTTP in a Modern World" to serve my websites over plaintext HTTP as well as HTTPS. See their article for the rationale.

Plaintext HTTP in a Modern World — joshua stein

I run the Caddy web server which has very strong defaults when it comes to HTTPS, which is wonderful. Let's Encrypt certificates are automatically provisioned and plaintext requests are unconditionally redirected to HTTPS. Implementing my goal just involves undoing a couple of the assumptions that Caddy makes about what you want.

Here's what I did

The first thing to do is add a http:// prefixed address to your site block in order to disable Caddy's HTTPS redirect. The same site will now be served over both ports 80 and 443.


lmcm.io, http://lmcm.io {
	...
}

But we do want clients that support HTTPS to use it over HTTP. To achieve this we conditionally redirect based on whether or not a request contains the Upgrade-Insecure-Requests header. Modern browsers include this header to indicate that they prefer HTTPS when possible.

To do this in Caddy, we first create a new named matcher (which I've called @upgrade) that matches all requests that have this header and are also made over HTTP. Without this second condition we'd create an infinite loop when browsers send the upgrade header in a HTTPS request, as they will sometimes do.


@upgrade {
  # The only valid value for this header is "1"
  header Upgrade-Insecure-Requests 1
  protocol http
}

Next, we handle requests that match @upgrade by redirecting to https://. We also set the Vary header, preventing the redirect being served by caches to browsers that might not support HTTPS.


handle @upgrade {
  # Using this weird placeholder instead of {uri} as I want the redirect to
  # reflect what the user sees in their address bar, not what the request
  # was rewritten to internally by the web server.
  redir https://lmcm.io{http.request.orig_uri.path} permanent
  header Vary Upgrade-Insecure-Requests
}

Lastly, if you make use of Strict Transport Security you should only send a HSTS header in response to HTTPS requests. This isn't essential but the header doesn't make any sense in a plaintext request and indeed browsers should ignore it in case it has been maliciously injected. We do this by defining another named matcher and making the HSTS header depend on it.

A complete working example


lmcm.io, http://lmcm.io {
  root * /var/www/lmcm.io

  @upgrade {
    header Upgrade-Insecure-Requests 1
    protocol http
  }

  handle @upgrade {
    redir https://lmcm.io{http.request.orig_uri.path} permanent
    header Vary Upgrade-Insecure-Requests
  }

  @https protocol https
  header @https Strict-Transport-Security "max-age=31536000;includeSubdomains"
}

Consequences

Is it okay to do this? While I'm convinced that many of the websites I create should be available over HTTP like this, I do want to make sure I'm doing as much as I can to make sure that browsers that do support HTTPS are using it.

Once a browser has visited one of my sites over HTTPS, HSTS means that they will continue to do so. But how to achieve that first secure visit? Upgrade-Insecure-Requests is well supported in modern browsers. According to caniuse.com it's been supported in Firefox and Google Chrome since 2015, Safari since 2017, and Edge since 2018. But that does leave a sizeable span of browser versions that support HTTPS but won't be sending this header.

If your domain happened to be HSTS preloaded when these older browsers last saw an update then there's a chance that you're good, but there's nothing you can do about it if not. Unfortunately, even if older browsers were periodically updating their preload lists outside of application upgrades the fact that we're not unconditionally redirecting from HTTP to HTTPS makes our sites ineligible for addition. Preloading also doesn't help at all for browsers pre ~2012.

I'm not aware of much else one can do, aside from ensuring that external links to your sites use HTTPS. If you really wanted to you could include a link to the HTTPS version of your site in pages served over HTTP along with an explanatory note — you'll see one on this site. I fully expect it never to be used, but then that's not really why we play with these things is it? ^^