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.
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.
- 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.
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):
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:
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.