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.
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, 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:
- I primarily develop on a Mac. Homebrew OpenSSL puts
openssl.cnf
in the directory/usr/local/etc/openssl/
and createscerts/
andprivate/
underneath it for certs and keys. - Chrome 58+ requires the SAN to match the domain name, not just the CN, so SAN is required.
- I create my projects under
~/development/${project_name}/
with the web root undertarget/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
}
}