Docker domain management

The background

I manage my services (typically) via domain names and HTTP HOST headers. This allows me to have memorable names, like blog.lolnope.us and plex.lolnope.us, rather than having to remember ports like lolnope.us:4433 and lolnope.us:32400.

An added benefit is the reuse of port 443 for multiple HTTPS-encrypted sites. I can decrypt all HTTPS-based traffic with a single "frontend", and then route it based on the HOST header.

You can also route HTTPS traffic without decrypting if your setup supports Server Name Indication.

Technology giveth, and technology taketh away

Well, some nice pros (and cons) come from such a set up, primarily revolving around HTTPS supporting features. One such feature, HSTS, can fool you. :(

HSTS, or HTTP Strict Transport Security, is a mechanism which locally forces your browser to respect the HTTPS-ness of a website without being fed an HTTP redirect.

Usually, the interaction looks like:

$ curl -vskL http://blog.lolnope.us > /dev/null
* Connected to blog.lolnope.us (24.4.223.218) port 80 (#0)
> GET / HTTP/1.1
> Host: blog.lolnope.us
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently  
< Server: nginx/1.13.7  
< Date: Mon, 02 Apr 2018 23:13:18 GMT  
< Content-Type: text/html  
< Content-Length: 185  
< Connection: keep-alive  
< Location: https://blog.lolnope.us/  
<  
* Cipher spec exchange...
> GET / HTTP/2
> Host: blog.lolnope.us
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/2 200  
< server: nginx/1.13.7  
< date: Mon, 02 Apr 2018 23:13:19 GMT  
< content-type: text/html; charset=utf-8  
< content-length: 14465  
< x-powered-by: Express  
< cache-control: public, max-age=0  
< etag: W/"3881-dyhYx/NYeK1AfEa7n0EikCDMvAM"  
< vary: Accept-Encoding  
< strict-transport-security: max-age=31536000  
<  
{ [3834 bytes data]
* Connection #1 to host blog.lolnope.us left intact

Here, I am being explicitly told via HTTP 301 that I need to go to Location: https://blog.lolnope.us/, and I'm being told that this website should use HSTS for a time: strict-transport-security: max-age=31536000.

Google Chrome remembers this HSTS header, and will HTTP 307 (Internal Redirect) you if you attempt to access it via HTTP:

Request URL: http://blog.lolnope.us/  
Request Method: GET  
Status Code: 307 Internal Redirect  
Referrer Policy: no-referrer-when-downgrade  

This comes from Chrome's Developer Tool's network inspector.

The browser never makes an HTTP request, it automatically "redirects" itself to HTTPS.

The problem here is... what if something is broken with the HTTP -> HTTPS redirect on the site itself?

Answer: Until that max-age expires, you won't have a clue. :(

Domains were busted

So if you haven't guessed it already, I discovered that my HTTP -> HTTPS redirect was broken for a time. The root cause had to do with how I was dealing with SNI, and routing HOSTs in my Docker environment.

The solution? Explicitly handle some redirects outside of nginx via a simple and lightweight Docker instance.

Using ianneub/redirect (because I'm too lazy to implement my own), I'm able to control what domains redirect to where:

  # Since this is listed first as a "backend", this is the default destination(?)
  # Redirect all "lolnope.us" requests to "www.lolnope.us"
  lolnope-us-redirect:
    container_name: lolnope-us-redirect
    depends_on:
      - nginx
    environment:
      VIRTUAL_HOST: lolnope.us
      LETSENCRYPT_HOST: lolnope.us
      LETSENCRYPT_EMAIL: [email protected]
      REDIRECT: 'https://www.lolnope.us'
    image: ianneub/redirect
    networks:
      - public
    restart: always

  # Redirect all "www.lolnope.us" requests to "blog.lolnope.us"
  www-lolnope-us-redirect:
    container_name: www-lolnope-us-redirect
    depends_on:
      - nginx
    environment:
      VIRTUAL_HOST: www.lolnope.us
      LETSENCRYPT_HOST: www.lolnope.us
      LETSENCRYPT_EMAIL: [email protected]
      REDIRECT: 'https://blog.lolnope.us'
    image: ianneub/redirect
    networks:
      - public
    restart: always

I then have a final backend which responds to blog.lolnope.us (which is this blog!). 📝

Putting it all together

Making an HTTP request to lolnope.us, all the way to landing on this blog:

$ curl -vskL http://lolnope.us > /dev/null
* Rebuilt URL to: http://lolnope.us/
* Connected to lolnope.us (24.4.223.218) port 80 (#0)
> GET / HTTP/1.1
> Host: lolnope.us
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently  
< Server: nginx/1.13.7  
< Date: Wed, 04 Apr 2018 23:55:05 GMT  
< Content-Type: text/html  
< Content-Length: 185  
< Connection: keep-alive  
< Location: https://lolnope.us/  
<  
* Issue another request to this URL: 'https://lolnope.us/'
* Connected to lolnope.us (24.4.223.218) port 443 (#1)
* Cipher spec exchange...
> GET / HTTP/2
> Host: lolnope.us
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/2 301  
< server: nginx/1.13.7  
< date: Wed, 04 Apr 2018 23:55:05 GMT  
< content-type: text/html  
< content-length: 185  
< location: https://www.lolnope.us/  
< strict-transport-security: max-age=31536000  
<  
* Issue another request to this URL: 'https://www.lolnope.us/'
* Connected to www.lolnope.us (24.4.223.218) port 443 (#2)
* Cipher spec exchange...
> GET / HTTP/2
> Host: www.lolnope.us
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/2 301  
< server: nginx/1.13.7  
< date: Wed, 04 Apr 2018 23:55:05 GMT  
< content-type: text/html  
< content-length: 185  
< location: https://blog.lolnope.us/  
< strict-transport-security: max-age=31536000  
<  
* Issue another request to this URL: 'https://blog.lolnope.us/'
* Connected to blog.lolnope.us (24.4.223.218) port 443 (#3)
* Cipher spec exchange...
> GET / HTTP/2
> Host: blog.lolnope.us
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/2 200  
< server: nginx/1.13.7  
< date: Wed, 04 Apr 2018 23:55:06 GMT  
< content-type: text/html; charset=utf-8  
< content-length: 14465  
< x-powered-by: Express  
< cache-control: public, max-age=0  
< etag: W/"3881-dyhYx/NYeK1AfEa7n0EikCDMvAM"  
< vary: Accept-Encoding  
< strict-transport-security: max-age=31536000  
<