Traditional Setup: Run Local Development Over HTTPS Using Caddy

Introduction

In this article I try to simplify for beginners, how to run local development environment (of different apps) over HTTPS using Caddy Server under same domain (ex: foo.bar).

If you wonder why we would need development runs over HTTPS. Well, there can be several reason, some of them:

  • Your application may rely on the assumption that it’s running over an SSL connection (ex: the Set-Cookie header has a Secure attribute which means the cookie will only be sent over a TLS/SSL connection).
  • Following 12-Factor App methodology:

X. Dev/prod parity states:

Keep development, staging, and production as similar as possible.

from The twelve-factor app

So running production over HTTPS but development over HTTP is a breach of this principle.

And — you may know this — if you are hosting your app on PaaS platform like Heroku in production, it runs behind reverse-proxy like nginx/caddy/etc. Those systems handle certificates and SSL termination for us. Locally we should have code/infrastructure in place to do the same.

Content

The Talk

  • Certificate Authority (CA) & SSL Certificate
  • Why Not Use Let’s Encrypt
  • Why Not Use Nginx
  • Reverse Proxy vs. Load Balancer
  • What We Are Going To Do

The Code

  • Create Local CA and Generate Local Trusted Certificate
  • Install and Configure Caddy as Reverse Proxy
  • Configure Caddy server for backend app
  • Configure Caddy server for frontend app
  • Run Project and Test All Configuration
  • Some Configuration for Vue.js

The Talk

Certificate Authority (CA) & SSL Certificate

A Certificate Authority is an entity that issues digital certificates.

An SSL Certificate is a popular type of Digital Certificate that binds the ownership details of a web server (and website) to cryptographic keys.

These keys are used in the SSL/TLS protocol to activate a secure session between a browser and the web server hosting the SSL Certificate. In order for a browser to trust an SSL Certificate, and establish an SSL/TLS session without security warnings, the SSL Certificate must contain the domain name of website using it, be issued by a trusted CA, and not have expired.

from globalsign website

So this is what we need, an SSL certificate that caddy server makes use of in the TLS configuration of our subdomains.

But, can we use real Certificate Authority (CA) to issue one for local development?

Using certificates from real certificate authorities (CAs) for development can be dangerous or impossible (for hosts like example.test, localhost or 127.0.0.1), but self-signed certificates cause trust errors. Managing your own CA is the best solution, but usually involves arcane commands, specialized knowledge and manual steps.

From the mkcert docs

To avoid this, we will learn how to use mkcert tool so we can create local CA and generate locally-trusted certificates.

Why Not Use Let’s Encrypt

Let’s Encrypt is a free, automated, and open certificate authority brought to you by the non-profit Internet Security Research Group (ISRG).

from letsencrypt website

To understand why we cannot use it, I recommend reading following links:

Why Not Use Nginx

nginx is fine, however its configuration can be pain for some, specially beginners.

caddy comes with good defaults and easy to configure. It’s a HTTP web-server that defaults to HTTP/2 and HTTPS. It can automatically generate certificates for you using Let’s Encrypt.

Reverse Proxy vs. Load Balancer

Both act as intermediaries in the communication between the clients and servers.

  • A reverse proxy accepts a request from a client, forwards it to a server that can fulfill it, and returns the server’s response to the client.
Image for post
  • A load balancer distributes incoming client requests among a group of servers, in each case returning the response from the selected server to the appropriate client.
Image for post

So here, we are not looking for balancing some load between apps (servers), we want a reverse-proxy which handles HTTPS requests for public-like domain to the right apps behind it.

What We Are Going To Do

We will learn how to create local CA, and generate local trusted certificate for *.foo.bar domain, so we can make use of it for two subdomains backend.foo.bar and frontend.foo.bar.

Then we will configure caddy to reverse proxy both the backend & frontend apps so we can run them over HTTPS connections behind it.

NOTE — I created two apps (a frontend app using Vue.js and a backend app using node.js) and configured caddy to work for a situation where the frontend app is communicating over HTTPS using cookies in secure mode to make actions that need authorization. Please refer to the repo for testing the whole project and see it works in your local environment.

The Code

I am Linux user, not sure how this works on Windows, I tested everything on Linux Mint 19.x which is based on Ubuntu 18.04. Feel free to understand the concepts, then apply them properly for your system.

Create Local CA and Generate Local Trusted Certificate

We need to install mkcert tool first to helps us for this (Please follow installation that fits your OS from here).

First, install certutil:

sudo apt install -y libnss3-tools

Then, install mkcert using linuxbrew:

sudo apt install -y linuxbrew-wrapper

brew install mkcert

Then, create local Certificate Authority:

mkcert -install

Then, generate local Trusted Certificate:

mkdir ./certs && cd ./certs

mkcert "*.foo.bar"

Finally, we need to add an entry for the subdomains in /etc/host file:

127.0.0.1 backend.foo.bar frontend.foo.bar

Now we have one local Trusted Certificate that we will configure caddy to use for both backend.foo.bar and frontend.foo.bar subdomains.

Install and Configure Caddy as Reverse Proxy

NOTE — I will be using v2_beta13.

First, download binary file for caddy server

# Download and rename downloaded file to 'caddy'

wget -O caddy \  
  [https://github.com/caddyserver/caddy/releases/download/v2.0.0-beta.13/caddy2_beta13_linux_amd6](https://github.com/caddyserver/caddy/releases/download/v2.0.0-beta.13/caddy2_beta13_linux_amd6)

Then, make it executable, and available through $PATH

chmod +x caddy

sudo mv ./caddy /usr/bin/caddy version   
v2.0.0-beta.13 h1:QL0JAepFvLVtOatABqniuDRQ4HmtvWuuSWZW24qVVtk=

Then, run it to confirm it is working, then kill it

caddy run

    2020/02/10 15:38:48.745    INFO    admin    admin endpoint started    {"address": "localhost 2019", "enforce_origin": false, "origins": ["localhost:2019"]}  
    2020/02/10 15:38:48.745    INFO    serving initial configuration

Configure Caddy server for backend app

Open it and add following configuration:

# Backend Address Configuration

backend.foo.bar { 

    tls ./certs/_wildcard.foo.bar.pem ./certs/_wildcard.foo.bar-key.pem

    reverse_proxy localhost:3000 {

        header_up Host                {host}
        header_up Origin              {host}
        header_up X-Real-IP           {remote}
        header_up X-Forwarded-Host    {host}
        header_up X-Forwarded-Server  {host}
        header_up X-Forwarded-Port    {port}
        header_up X-Forwarded-For     {remote}
        header_up X-Forwarded-Proto   {scheme}

        header_down Access-Control-Allow-Origin       https://frontend.foo.bar
        header_down Access-Control-Allow-Credentials  true

    }
}

This will create configuration for website address backend.foo.bar, tls directive is pointing to generated pem files for the trusted certificate. reverse_proxy directive is for serving this domain as reverse proxy to one backend which is localhost:3000. header_up option is for manipulating request header going upstream to the backend (Keep all of it for most basic configurations). header_down option is for manipulating response header coming downstream from the backend. I needed to add header_down options for Access-Control-Allow-Origin and Access-Control-Allow-Credentials for the sake of the use case as mentioned before.

Configure Caddy server for frontend app

Open Caddyfile and add following configuration:

# Frontend Address Configuration

frontend.foo.bar {

    tls ./certs/_wildcard.foo.bar.pem ./certs/_wildcard.foo.bar-key.pem

    reverse_proxy localhost:8080 {
        header_up Host                "localhost"
        header_up X-Real-IP           {remote}
        header_up X-Forwarded-Host    "localhost"
        header_up X-Forwarded-Server  "localhost"
        header_up X-Forwarded-Port    {port}
        header_up X-Forwarded-For     {remote}
        header_up X-Forwarded-Proto   {scheme}
    }
}

Almost same configuration, but please notice the difference in values of request headers of header_up option.

Run Project and Test configuration

Open terminal, and run caddy server:

caddy run --config Caddyfile

    2020/02/17 11:01:26.848 INFO    using provided configuration    {"config_file": "Caddyfile", "config_adapter": ""}  
    ...
    2020/02/17 11:01:26.859 INFO    http    skipping automatic certificate management because one or more matching certificates are already loaded  {"domain": "frontend.foo.bar", "server_name": "srv0"}  
    2020/02/17 11:01:26.859 INFO    http    skipping automatic certificate management because one or more matching certificates are already loaded  {"domain": "backend.foo.bar", "server_name": "srv0"}
    2020/02/17 11:01:26.860 INFO    serving initial configuration

Then open another terminal, and run the backend app:

npm run dev
    [nodemon] 2.0.2  
    [nodemon] to restart at any time, enter `rs`  
    [nodemon] watching dir(s): *.*  
    [nodemon] watching extensions: js,mjs,json  
    [nodemon] starting `babel-node ./src/bin/server.js`Server listening on port 3000

Test backend using rest client (ex: insomnia):

Image for post

Then open another terminal, and run the frontend app:

npm run serve
    INFO  Starting development server...
    DONE  Compiled successfully in 27732ms  7:23:33 PMApp running at:  
        - Local:   [http://localhost:8080/](http://localhost:8080/)  
        - Network: [https://frontend.foo.bar/](https://frontend.foo.bar/)

    Note that the development build is not optimized.  
    To create a production build, run npm run build.

Test frontend from browser:

Image for post

Now both apps being served successfully through caddy reverse proxy.

Now, we need to test that cookies are transferred securely over HTTPS between the frontend and the backend, and make sure authentication is setup and configured properly over that local HTTPS. Following video demonstrates this part:

Some Configuration for Vue.js

Last part, things didn’t work just like that with default Vue.jsv configuration. I had following issue with HMR (Hot Module Reload):

// Chrome Console  

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at [https://192.168.5.10:8080/sockjs-node/info?t=1888826474028.](https://192.168.5.10:8080/sockjs-node/info?t=1888826474028.) (Reason: CORS request did not succeed).
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at [https://localhost/sockjs-node/info?t=4740717588882.](https://localhost/sockjs-node/info?t=4740717588882.) (Reason: CORS request did not succeed).

And I had to set public field of devServer entry in vue.config.js file with frontend domain:

// vue.config.js
module.exports = {
    ...
    devServer: {
        public: 'https://frontend.foo.bar'
    }
}

And, I also got following issue:

// Chrome Console
Invalid Host Header

And I had to set allowedHosts field of devServer the local domain name foo.bar:

// vue.config.js
module.exports = {
    ...
    devServer: {
        public: 'https://frontend.foo.bar',
        allowedHosts: ['.foo.bar']
    }
}

And, that’s it. Everything works fine now.

I hope the article is useful and you learned from it. If you think something is wrong or you have any question please feel free to leave comment and I will do my best.

Thanks for reading 🌹 and hope you like it.


References

No Comments Yet