DEV Community

Cover image for HTTPS In Development: A Practical Guide
Kmaschta
Kmaschta

Posted on • Updated on • Originally published at marmelab.com

HTTPS In Development: A Practical Guide

According to Firefox Telemetry, 76% of web pages are loaded with HTTPS, and this number is growing.

Sooner or later, software engineers have to deal with HTTPS, and the sooner the better. Keep reading to know why and how to serve a JavaScript application with HTTPS on your development environment.

HTTPS adoption according to Firefox Telemetry

Why Use HTTPS On a Development Environment?

First, should you serve a website in production through HTTPS at all? Unless you really know what your are doing, the default answer is yes. It improves your website at so many levels: security, performance, SEO, and so on.

How to setup HTTPS is often adressed during the first release, and brings a lot of other questions. Should traffic be encrypted from end to end, or is encryption until the reverse proxy enough? How should the certificate be generated? Where should it be stored? What about HSTS?

The development team should be able to answer these questions early. If you fail to do so, you might end up like Stack Overflow wasting a lot of time.

Besides, having a development environment as close as possible from the production reduces risks that bugs reach the production environment, and also tend to decrease the time to debug those bugs. It's also true for end-to-end tests.

In addition, there are features that only work on a page served by HTTPS, such as Service Workers.

But HTTPS is slow! Many people believe that encryption is complicated and in a certain way must be slow to be efficient. But with modern hardware and protocols, this is not true anymore.

How To Generate A Valid Certificate For A Development Environment?

For production systems, it's easy to get a TLS certificate: generate one from Let's Encrypt or buy one from a paid provider.

For the development environment, it seems trickier, but it isn't that hard.

Mkcert: The No Brainer CLI

Filippo Valsorda recently published mkcert, a simple cli to generate locally-trusted development certificates. You just have to run a one-line command:

mkcert -install
mkcert example.com

The fully supported certificate will be available where your ran the command, namely at ./example.com-key.pem.

Manual Installation With OpenSSL

mkcert should fulfill all of your needs, unless you have to share the same certificate with your coworkers, or through other systems than your local env. In that case, you can generate your own certificate thanks to openssl.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt

The certificate (server.crt) and its key (server.key) will be valid but self-signed. This certificate will be unknown to any Certificate Authority. But all browsers ask well-known certificate authorities to validate certificates in order to accept encrypted connections. For a self-signed certificate, they can't validate it, so they display an annoying warning:

Self-Signed Certificate Error

You can accept that inconvenience and manually ignore the warning each time it shows up. But it's very cumbersome, and it may block e2e tests in a CI environment. A better solution is to create your own local certificate authority, add this custom authority to your browser and generate a certificate from it.

That's what mkcert does for you under the hood, but if you want to do it yourself, I wrote a gist that may help you: Kmaschta/205a67e42421e779edd3530a0efe5945.

HTTPS From a Reverse Proxy Or A Third-Party App

Usually, end-users don't directly reach the application server. Instead, user requests are handled by a load balancer or a reverse proxy that distributes requests across backends, stores the cache, protects from unwanted requests, and so on. It's not uncommon to see these proxies take the role of decrypting requests and encrypting responses as well.

On a development environment, we can use a reverse proxy, too!

Encryption via Traefik and Docker Compose

Traefik is a reverse proxy that comes with a lot of advantages for developers. Among others, it's simple to configure and it comes with a GUI. Also, there is an official docker image available on docker hub.

So, let's use it inside the docker-compose.yml of a hypothetical application that only serves static files:

version: '3.4'

services:
    reverse-proxy:
        image: traefik # The official Traefik docker image
        command: --docker --api # Enables the web UI and tells Traefik to listen to docker
        ports:
            - '3000:443'  # Proxy entrypoint
            - '8000:8080' # Dashboard
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events
            - ./certs/server.crt:/sslcerts/server.crt
            - ./certs/server.key:/sslcerts/server.key
            - ./traefik.toml:/traefik.toml # Traefik configuration file (see below)
        labels:
            - 'traefik.enable=false'
        depends_on:
            - static-files
    static-files:
        image: halverneus/static-file-server
        volumes:
            - ./static:/web
        labels:
            - 'traefik.enable=true'
            - 'traefik.frontend.rule=Host:localhost'
            - 'traefik.port=8080'
            - 'traefik.protocol=http'
        ports:
            - 8080:8080

In this example, our static file server listens on port 8080 and serves files in HTTP. This configuration tells Traefik to handle HTTPS requests to https://localhost and proxy each of them to http://localhost:8080 in order to serve static files.

We also have to add a traefik.toml to configure the Traefik entry points:

debug = false

logLevel = "ERROR"
defaultEntryPoints = ["https","http"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  [entryPoints.https.tls]
      [[entryPoints.https.tls.certificates]]
      certFile = "/sslcerts/server.crt"
      keyFile = "/sslcerts/server.key"

Here, we have two entry points: http and https, listening respectively to ports 80 and 443. The first one redirects to the HTTPS, and the second is configured to encrypt requests thanks to the specified TLS certificates.

Traefik Dashboard

Encryption From Docker Compose via Nginx

Obviously, we can do exactly the same with the popular Nginx reverse proxy. As Nginx can also directly serve static files itself, the setup is simpler. Again, the first step is the docker-compose.yml:

version: '3'

services:
    web:
        image: nginx:alpine
        volumes:
            - ./static:/var/www
            - ./default.conf:/etc/nginx/conf.d/default.conf
            - ../../certs/server.crt:/etc/nginx/conf.d/server.crt
            - ../../certs/server.key:/etc/nginx/conf.d/server.key
        ports:
            - "3000:443"

And the nginx configuration at default.conf:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;

    server_name ~.;

    ssl_certificate /etc/nginx/conf.d/server.crt;
    ssl_certificate_key /etc/nginx/conf.d/server.key;

    location / {
        root /var/www;
    }

    ## If the static server was another docker service,
    ## It is possible to forward requests to its port:
    # location / {
    #     proxy_set_header Host $host;
    #     proxy_set_header X-Real-IP $remote_addr;
    #     proxy_pass http://web:3000/;
    # }
}

Serving HTTPS Directly From The Application

Sometimes security requirements demand end-to-end encryption, or having a reverse proxy just might seem to be overkill on a development environment. Most of the time, it's possible to serve HTTPS directly from your everyday development environment.

Let's take the example of a common stack: a React application with a REST API using express.

Using Create React App or Webpack Dev Server

Your average React app is bootstraped by create-react-app. This awesome tool comes with a lot of built-in features and can handle HTTPS out of the box. To do so, you just have to specify a HTTPS=true environment variable when starting the app:

HTTPS=true npm run start
# or
HTTPS=true yarn start

This command will serve your app from https://localhost:3000 instead of http://localhost:3000 with an auto-generated certificate. But it's a self-signed certificate, so the developer experience is poor.

If you want to use your own HTTPS certificate (signed with an authority that your browser trusts), create-react-app doesn't let you configure it without ejecting the app (npm run eject).

EDIT: A dev.to reader, Zwerge, found a clever workaround to replace the default HTTPS certificate on the fly:

  "scripts": {
    "prestart": "(cat ../../certs/server.crt ../../certs/server.key > ./node_modules/webpack-dev-server/ssl/server.pem) || :",
    "start": "react-scripts start",
  },

Fortunately, if you do eject CRA, or if your project is bundled with webpack, webpack-dev-server is as straightforward as create-react-app when it comes to serve HTTPS! It's possible to configure a custom HTTPS certificate with two lines in the Webpack configuration:

const fs = require('fs');
const path = require('path');

module.exports = {
    mode: 'production',
    // ...
    devServer: {
        https: {
            key: fs.readFileSync(path.resolve(__dirname, '../../certs/server.key')),
            cert: fs.readFileSync(path.resolve(__dirname, '../../certs/server.crt')),
        },
        port: 3000,
    },
};

The next time you'll run webpack-dev-server, it will handle HTTPS requests to https://localhost:3000.

Example App - Static Site

Encrypted HTTP/2 With Express And SPDY

Now that we have our frontend part of the app that is served through HTTPS, we have to do the same with our backend.

For this purpose, let's use express and spdy. No wonder why these two libraries names are about SPEED, it's because they are fast to setup!

const fs = require('fs');
const path = require('path');
const express = require('express');
const spdy = require('spdy');

const CERTS_ROOT = '../../certs/';

const app = express();

app.use(express.static('static'));

const config = {
    cert: fs.readFileSync(path.resolve(CERTS_ROOT, 'server.crt')),
    key: fs.readFileSync(path.resolve(CERTS_ROOT, 'server.key')),
};

spdy.createServer(config, app).listen(3000, (err) => {
    if (err) {
        console.error('An error occured', error);
        return;
    }

    console.log('Server listening on https://localhost:3000.')
});

HTTP/2 isn't required to serve HTTPS, it's possible to serve encrypted content with HTTP first of the name, but while we are at serving HTTPS, we can upgrade the HTTP protocol. If you want to know more about the advantages of HTTP/2, you can read this quick FAQ.

Conclusion

Modern tooling allows to build applications that are safer and faster for end-users and, now, easy to bootstrap. I hope that I convinced you to use these libraries and technologies starting from your project inception, when they are still cheap to install.

All the examples I used in this blog post are gathered on the following repo: marmelab/https-on-dev. Feel free to play with and add your own HTTPS development experience!

Top comments (15)

Collapse
 
zwergius profile image
Christian

I did implement this in CRA yesterday with no need for ejecting.

3 steps:

mkcert localhost

cat ~/.localhost-ssl/localhost-key.pem ~/.localhost-ssl/localhost.pem > {cra-path}/cert/server.pem

and then in package.json

"prestart": "cp -f ./cert/server.pem ./node_modules/webpack-dev-server/ssl || :",

Collapse
 
justinobney profile image
Justin Obney • Edited

Would you happen to know what this would look like on Windows? I see mkcert works fine on Windows..

UPDATE:

I have this working on Windows using mkcert & customize-cra

config-overrides.js

const fs = require('fs');
const path = require('path');
const {overrideDevServer} = require('customize-cra');

const configureHttps = () => config => {
  return {
    ...config,
    https: {
      key: fs.readFileSync(path.resolve(__dirname, './localhost+2-key.pem')),
      cert: fs.readFileSync(path.resolve(__dirname, './localhost+2.pem')),
    },
  };
};

/* config-overrides.js */
module.exports = {
  devServer: overrideDevServer(
    // dev server plugin
    configureHttps()
  ),
};
Collapse
 
kmaschta profile image
Kmaschta

Oh nice, thanks!
Can I add this snippet to the blog post and cite you as well?

Collapse
 
zwergius profile image
Christian

Of course! :)

Collapse
 
tcelestino profile image
Tiago Celestino

nice hook.

Collapse
 
nssimeonov profile image
Templar++

Why do you do that? Why create certificates like this when you have letencrypt?

Here's a hint: can register a real domain really cheap - just one then make subdomains for each of you projects - many registrars will provide you with a dns server as well, then create a real certificate via LetsEncrypt. This way you dev web server can be accessed from all of your coworkers and even customers without any issue. The effort isn't much different anyway.

Collapse
 
kmaschta profile image
Kmaschta

By development environment, I mostly meant "localhost".
So, sure it's possible to update /etc/hosts and get a certificate from LetsEncrypt, but mkcert is so much simpler!

Collapse
 
vampiire profile image
Vamp

Fantastic mate thank you.

Collapse
 
jimmyadaro profile image
Jimmy Adaro

I did a simple bash script to create a TLS certificate, add it to your macOS Keychain and also to your XAMPP Virtualhost file with a simple command. Check it here: github.com/jimmyadaro/secure-vhost

Collapse
 
mario_tilli profile image
Mario Tilli

I have to admit that I've never thought to use HTTPS in a development environment: that's weird! So thank you very much for this post!!

Collapse
 
stenpittet profile image
Sten

mkcert is the one piece I've been missing! Thank!

Collapse
 
ashleyjsheridan profile image
Ashley Sheridan

The default answer to whether or not to use HTTPS should not be yes. If you don't know how to answer the question, then ask the help of someone who can answer.

HTTPS can actually have a huge impact on performance. Content is unable to be cached correctly by many systems (e.g. proxy caching), and this can actually lead to the web being completely unusable: thenextweb.com/contributors/2018/0...

Collapse
 
sachin19219 profile image
Sachin19219

Wow, excellent post, with great research done on putting linked articles.Thanks a lot for sharing.

Collapse
 
lampewebdev profile image
Michael "lampe" Lazarski

WoW! Great post! Thank you!

Collapse
 
ploppy1337 profile image
ploppy

I‘d just wish I could tell the browser to trust my CA only for specific domains. Make sure to keep your private key secret, or it my be used to spoof other websites...

Collapse
 
thomasbnt profile image
Thomas Bnt ☕

Woah, greaaat post!