Skip to content

Instantly share code, notes, and snippets.

@benjaminblack
Last active September 4, 2018 21:40
Show Gist options
  • Save benjaminblack/338368c64c0c2b9f6a777b23c4af095f to your computer and use it in GitHub Desktop.
Save benjaminblack/338368c64c0c2b9f6a777b23c4af095f to your computer and use it in GitHub Desktop.
Using HTTPS in local development

Using HTTPS in local development

Beginning June 2018, Chrome 68 will begin marking HTTP as "Not Secure". Other browsers may swiftly follow Chrome's lead.

The push for Secure Contexts Everywhere and enforcement of HTTPS for all new features is also motivation to secure local development environments, to reduce the delta between development and production environments.

In short, the Web is rapidly moving all traffic to HTTPS, and local development environments should be no different.

The Problem

Easing the use of HTTPS in local environments, where each project gets a domain like ${project_name}.localhost, means automating the process of creating a self-signed certificate for each project, configuring the web server to use it, and locally trusting the certificates. (A new certificate for each project is required in this configuration, because a certificate cannot cover an entire TLD like *.localhost.)

Automating web server configuration with Nginx and standard certificates necessitates creating a server block for each project, because Nginx cannot use variables in the ssl_certificate or ssl_certificate_key rules, only strings; therefore, each project must hard-code the path to its certificate and key.

Automating trust of self-signed certificates can also be a hurdle.

My Solution

My solution, which requires only a single certificate and a single server block configuration, is to create a self-signed wildcard certificate for *.dev.localhost, which only needs to be created and trusted once, and configure Nginx to serve individual projects as ${project_name}.dev.localhost with TLS.

(I also create self-signed certs for *.prod.localhost for production builds; the following instructions are the same, but are written *.dev.localhost.)

From a security/namespace perspective, this is mostly fine: the Same-Origin policy has host granularity, so alpha.dev.localhost has no access to beta.dev.localhost or v.v.

To comply with Mozilla Observatory's "Modern" compatibility for TLS, we can generate an ECDSA certificate using the secp384r1 curve and SHA512 for the signature hash (SHA512 performs better than SHA256 on 64-bit systems).

Notes:

  1. I primarily develop on a Mac. Homebrew OpenSSL puts openssl.cnf in the directory /usr/local/etc/openssl/ and creates certs/ and private/ underneath it for certs and keys.
  2. Chrome 58+ requires the SAN to match the domain name, not just the CN, so SAN is required.
  3. I create my projects under ~/development/${project_name}/ with the web root under target/dev/.

Generating the key and cert:

$ openssl ecparam -genkey -name secp384r1 -out /usr/local/etc/openssl/private/wildcard-dev-localhost-key.pem

$ openssl req -x509 -sha512 -nodes -days 365 \
-key /usr/local/etc/openssl/private/wildcard-dev-localhost-key.pem \
-subj "/CN=dev.localhost" -reqexts SAN -extensions SAN \
-config <(cat /usr/local/etc/openssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:dev.localhost,DNS:*.dev.localhost')) \
-out /usr/local/etc/openssl/certs/wildcard-dev-localhost-cert.pem

Sanity-checking the key and cert:

$ openssl ec -in /usr/local/etc/openssl/private/wildcard-dev-localhost-key.pem -noout -text
read EC key
Private-Key: (384 bit)
[...]
ASN1 OID: secp384r1
NIST CURVE: P-384


$ openssl x509 -in /usr/local/etc/openssl/certs/wildcard-dev-localhost-cert.pem -noout -text
Certificate:
    [...]
    Signature Algorithm: ecdsa-with-sha512
        Issuer: CN=dev.localhost
        [...]
        Subject: CN=dev.localhost
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                [...]
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:dev.localhost *.dev.localhost
[...]

Configuring Nginx:

server {
    listen 80;
    listen [::]:80;
    server_name *.dev.localhost;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate /usr/local/etc/openssl/certs/wildcard-dev-localhost-cert.pem;
    ssl_certificate_key /usr/local/etc/openssl/private/wildcard-dev-localhost-key.pem;
    
    # For "Modern" compatibility (Nginx 1.13 with OpenSSL 1.0.2)
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_protocols TLSv1.3 TLSv1.2; # TLSv1.3 is supported in Nginx 1.13
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA\
256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
    ssl_prefer_server_ciphers on;

    server_name ~^(?<project>[^\.]+)\.dev\.localhost$;

    root /Users/bb/development/$project/target/dev;

    location / {
        try_files $uri $uri/ /index.html; # generic SPA
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment