loading...
Cover image for Local development with HTTPS on OSX
Tability

Local development with HTTPS on OSX

stenpittet profile image Sten ・5 min read

A good rule of thumb in software development is that you should try to be as close as possible from your production environment. Doing so helps you avoid common mistakes due to discrepancies between your local dev setup and your prod configuration. Containers are really helpful to mitigate some of the dependencies issues, but one thing that has always been a PITA for me is using HTTPS for local development.

At Squadlytics we have a React app for our UI and a Rails API backend where we force the use of SSL. We've tried many things to find an easy way to use HTTPS for local development, and we finally found a solution using a proxy and dnsmasq. I'll share here how we're doing things, but first, let me present you the alternatives that we explored.

Option 1: Generating certificates ourselves

We started by looking at generating our own certificates for local development. First, you need to create your own Certificate Authority. Then you use that entity to build your development certificates.

There's a great tutorial written by Brad Touesnard that you can read if you want to go down that path. It looked great at the beginning, but it started to become cumbersome when we had to figure out custom starting scripts for the backend and the frontend to use the right certificates.

We had to do a few different takes to get it right, and we ended up looking for a simpler solution where we wouldn't have to manage the certificates ourselves.

Pros: Once it's done it just works.
Cons: A bit hard to setup and manage.

Option 2: Using ngrok

If you're not yet using ngrok chances are that you'll need it sooner or later. It's a neat tool that allows you to create tunnels to expose your local dev machine to the web. The typical use case is to test webhooks, but you can also reserve your domains and generate SSL tunnels to your dev environment with either a subdomain (https://subdomain.ngrok.io) or use your own dev domain for it (https://api.squadlytics.dev).

It minimized the configuration on our side. You only need to install ngrok to get it working - and you won't be getting certificate errors in your browser.

But problems start if you need to share dedicated URLs. For instance, if your backend API is accessible at https://api.ngrok.io, you can only tunnel this URL to one local dev machine. To my knowledge, you'd have to reserve custom domains for each developer to solve that problem (https://api.john.ngrok.io, https://api.jane.ngrok.io) and probably do some tricks in your code to support that. On top of that, every request needs to go through ngrok which slows down development and prevents you to code while being offline.

So we ditched ngrok as it would not scale for our local SSL dev issue. I want to emphasize here that it's a great tool, and it's mostly our fault for trying to stretch it too far.

Pros: easy to get started, great for testing webhooks
Cons: slow for development, doesn't scale for continuous use

Option 3: local-ssl-proxy + dnsmasq

We looked back at the constraints we had for local development to simplify our problem:

  • We needed to be able to hit specific domains for the API and authentication.
  • We were using subdomains for workspaces (à la Slack) and therefore required to access a wide range of subdomains locally.
  • We needed all the requests to be done via HTTPS.
  • We wanted to have something quick to set up for extra services, and easy to install for new developers.
  • Being able to code offline would have been a big plus.

With these goals in mind, we settled for a simple solution using a proxy server and a custom local DNS server.

local-ssl-proxy

local-ssl-proxy is a npm package that starts a simple SSL HTTP proxy using a self-signed certificate. We created a small configuration file to map the Rails backend and the React frontend to different ports for SSL.

{
  "Rails backend": {
    "source": 3001,
    "target": 3000
  },
  "React UI": {
    "source": 8090,
    "target": 8080
  }
}

With that configuration, we could use https://api.squadlytics.dev:3001 and https://acme.squadlytics.dev:8090 to have access to use the platform via HTTPS. No need to create a custom starting script for Rails or figuring out how to pass a certificate to the React app.

Adding new services would be as simple as adding a new entry in that configuration file.

dnsmasq

The other problem we needed to solve was to support a wildcard domain to avoid having to create multiple entries in /etc/hosts. Thankfully you can use dnsmasq for that and create a local DNS server with your own rule.

You can follow the instructions in this Gist to see how you can set it up on your machine. In our case, we created a bash script to do it all at once.

You should be careful not to set a wildcard domain for all .dev domains as this is a real TLD. This is why we've restricted our config to only map .squadlytics.dev domains to localhost instead of supporting all URLs using .dev.

#!/bin/bash
echo "1. Installing dnsmasq"
brew install dnsmasq
echo "2. Configuring *.squadlytics.dev local domains"
mkdir -pv $(brew --prefix)/etc/
echo 'address=/squadlytics.dev/127.0.0.1' > $(brew --prefix)/etc/dnsmasq.conf
sudo cp -v $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons
sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
sudo mkdir -v /etc/resolver
sudo bash -c 'echo "nameserver 127.0.0.1" > /etc/resolver/dev'

Some people recommend using a fake TLD (like .test), but services like Google require real TLDs for OAuth.

Don't forget to add your localhost as a DNS server in your network settings otherwise it will not work.

Bypassing the SSL certificate errors

The last step for us is to bypass the certificate errors that will be inevitably thrown by your browsers.

If you're using Safari, you can just accept to visit the site in the error screen. Just make sure that you do that not only for your UI but also for any API that is used by the client.

Chrome seems to be much more strict, and to this day I've only managed to bypass the errors by launching Chrome with the --ignore-certificate-errors flag.

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --ignore-certificate-errors &> /dev/null &

I hope this will be helpful to some of you. You can check our config scripts in our repo at https://github.com/squadlytics/local-ssl-development. If you have questions or suggestions, please don't hesitate to write a comment.

Photo by Liam Tucker on Unsplash

Posted on by:

stenpittet profile

Sten

@stenpittet

Co-founder @Tability, previously @Atlassian

Tability

Tability is an accountability platform for teams. Add your goals and projects, and we make sure that you won't forget about them.

Discussion

pic
Editor guide
 

These very few lines of python do it for me with a self signed certificate:

import BaseHTTPServer, SimpleHTTPServer
import ssl

httpd = BaseHTTPServer.HTTPServer(('localhost', 4443), SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, certfile='./server.pem', server_side=True)
httpd.serve_forever()

 

FWIW, I made a little cross-platform CLI tool called tls-keygen. It generates modern localhost certs (ECC, SAN, IPv6, wildcard, ...) and hooks up the OS trust store to satisfy browser security requirements.

Give it a try and let me know what you think:
npmjs.com/package/tls-keygen

 

I'm not even sure what you're advocating after reading this. Instead of simply using self-signed certificates (which are super easy to automate in a Vagrant box) and then using --ignore-certificate-errors, you're saying you're using several components to achieve the same end result?

If it's crucial for you the domain stays the same, I don't understand how your code has been built without such basic configuration ability.

The other option that I can imagine you could be doing based on this, is that you're putting your production SSL certificates on your developers' computers, which is a BAD idea.

Personally my strategy tends to be:

  • Buy a 100% valid SSL certificate for *.dev.myapp.com or dev.myapp.com (they cost very little) - best deals I know of are from ssls.com
  • Deploy that on your development VMs, or in your repository and configure your apps to use it the same way you configure your production environment to use that certificate (generally terminating it to Nginx/HAProxy/similar)
  • Configure the VMs with hosts entries so api.dev.myapp.com or whatever else you have point to 127.0.0.1
  • Configure the hosts entries on the host machine either manually or via something like vagrant-hostsupdater

No worrying about self-signed certificates causing errors or having your devs ignore invalid certificates, no worrying about your devs having production certificates, no worrying about difficult complicated processes to follow - you deploy it the same way as to production, just using a different certificate.

 

After thinking of this a bit more, I really hope you're not saying your devs are running your backend and frontend code directly on their main OS. You're not likely running OSX on your production servers, and that kind of discrepancy alone can lead to issues, even if your devs would never work on any other thing on that same laptop (which is unlikely).

 

Thanks for the Vagrant tip, part of our stack is on Heroku (small team, rather pay Heroku to deal with Ops) so I'm not sure it would solve the "mirror prod" issue. What we do instead is to use Docker to make sure we all use the same versions of the dependencies (languages and services) and to be as close as possible to the Heroku stack.

We have a monolith for the backend and our frontend on S3 + Cloudfront so our current approach was fitting our use case but I definitely want to switch to proper certificates as soon as possible.

I'm definitely not proud of the --ignore-certificate-errors as it's a risk in itself.

 

I have the same problem at my job, but we just use webpack-dev-server, it has an option to enable HTTPS. If you enable it and don't provide a certificate it just automatically auto-generates a self-signed one for you which you need to manually allow in your browser or add to your certificate manager. One caveat is that whenever you make a clean npm install it generates a new certificate which you need to allow again.

If your project is not node you should still be able to use it in conjunction with the proxy option to forward requests to another localhost port. This solution would be similar to local-ssl-proxy.

 

Yes, we got that working out of the box on our React app, but we had to do custom startup scripts for the other services.

It's definitely a great feature.

 

I did some bash scripts to automate this process a while ago. I prefer haproxy for easy of use.

You could check the code here github.com/mariancraciun1983/Dev-C... with a haproxy example.

Another simple solution for local aliasing, would be to have a CNAME for *.localhost.DOMAIN.com point to 127.0.0.1 . This will allow you to have unlimited hostnames.

 

Seems quite elegant, I'll definitely look into that. Thanks for sharing your code too!

 
 
 

I use Laravel Valet, laravel.com/docs/5.6/valet, which has nginx, dnsmasq setup in a single package with a custom ld, I am using .test

You can get an HTTPS valid site by using the secure option for a site.

 

backplane.io will solve this for you, and takes less than two minutes to set up.