Create a local HTTPS proxy server

In recent months I’ve been working to add Apple Pay for Web to a major clothing retailer. One of the requirements for Apple Pay for Web is that the connection must be over HTTPS. Most of the time when I’m developing locally, I do not use HTTPS. Local, meaning the application code is running on my laptop. In most cases, HTTPS is just run in staging and production environments and not handled directly by your app code.

So this posed a problem. I didn’t want to work on the server where it provides an HTTPS connection, that’s a pain. Also, if you’re not familiar with HTTPS servers, you need to have a valid certificate and more. So this solution requires a few steps to get working, but once it does, it works nicely.

TL;DR

  1. Create a fake SSL certificate (e.g. www.fake-example.com)
  2. Create a docker container that uses Nginx to proxy localhost:443 => 172.16.123.1:3000
  3. Override your local /etc/hosts file
  4. Start your app on localhost:3000
  5. Hit your app in the browser at https://www.fake-example.com

Creating a fake SSL certificate

It’s most likely not a good idea to use your production SSL certificate and key when doing local development, so you’ll want to create a fake version.

It’s entirely possible to purchase legitimate SSL certifates and use them, but this is not always possible, and it’s free to make your own

Update this code and run it on a Mac or Linux machine

sudo openssl req -x509 -sha256 -newkey rsa:2048 -keyout cert.key -out cert.pem -days 1024 -nodes -subj '/CN=www.fake-example.com'

You now have files called cert.key and cert.pem for a certificate that is valid for the next 10 years.

Caveat: Your browser will not like this certifcate, but since you trust it, you can override the browser to accept the certificate and not complain.

Save those files, we’ll use them in just a minute

Create a docker container

Now we want to create a docker container that will use our certificate and proxy all requests to our app running on port 3000. We will use docker compose to accomplish the configuration of the container.

Create a directory

First, you’ll want to create a directory where you can hold the docker compose yaml file, the nginx configuration, and the ssl certificates. Mine looks like this:

.
├── docker-compose.yml
├── proxy.conf
└── ssl
    ├── cert.key
    └── cert.pem

The Docker Compose file

The docker-compose.yml file is really pretty short. Here it is.

version: '2'

services:
  nginx:
    image: nginx:stable-alpine
    volumes:
      - ./proxy.conf:/etc/nginx/conf.d/default.conf
      - ./ssl:/etc/nginx/ssl
    ports:
      - 443:443
    extra_hosts:
      - "dockerhost:${LOCAL_IP}"

As you can see: * We pull down the nginx image using the small linux alpine version. * Then we mount some volumes inside the container, the proxy.conf file and the ssl folder that holds our certs. * Next the configuration tells the container to listen on port 443 and forward internall to port 443 * What is the extra_hosts configration? * Glad you asked, let’s cover that in this next section

The localhost alias

The problem we encounter here is allowing the container to contact the host machine. Since Nginx is running inside the Docker container and your app is running outside of the docker container, locally on port 3000, the container needs to know how to reach the host machine.

This is not as easy as it would appear.

There are several solutions to this. On a Mac, the best way I’ve found is to create an alias to localhost. Simply run this command will create an alias to locahost.

sudo ifconfig lo0 alias 172.16.123.1

On Windows, I’m not sure, sorry.

Back to extra_hosts

Notice that I have a reference to ${LOCAL_IP}? Well, I have an environment variable that is just LOCAL_IP=172.16.123.1. In the docker-compose.yml file we assign that IP to the hostname dockerhost.

DANGER! However tempting, Do NOT name your environment variable DOCKER_HOST, you’ll break your docker setup. The reader can investigate why or ping me for the answer.

Docker Compose files can utilize environment variables, nifty. Moving on.

The Nginx configuration

Now lets take a look at our proxy.conf file.

server {
    listen                     443 ssl;
    server_name                localhost;

    ssl                        on;
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_certificate            ssl/cert.pem;
    ssl_certificate_key        ssl/cert.key;

    location / {
        proxy_pass          http://dockerhost:3000;
        proxy_set_header    Host      $host;
        proxy_set_header    X-Real-IP $remote_addr;
        proxy_set_header    X-HTTPS   'True';
    }
}

In here you’ll notice a few items of interest: * We specify our cert and key as living in the ssl/* folder we mounted in our docker-compose.yml file * We are proxy passing all traffic to dockerhost:3000, this will resolve to your localhost:3000 on your machine. * The rest is just standard configuration for setting up Nginx as a proxy.

Start your docker container

Inside your folder that has the docker-compose.yml file, all you need to do is run docker-compose up -d. That will start your HTTPS proxy and put it in the background.

Check if it started successfully by running docker-compose ps

       Name                Command          State              Ports
--------------------------------------------------------------------------------
ssltesting_nginx_1   nginx -g daemon off;   Up      0.0.0.0:443->443/tcp, 80/tcp

Override your local /etc/hosts file

Now we need to tell our computer that www.fake-example.com is not something it should ask DNS about, but can get it’s answer right from /etc/hosts. Just so you understand, when your browser needs to resolve an address to a domain name, it will first look in /etc/hosts, then ask DNS.

So we just need to add this to our /etc/hosts file.

127.0.0.1   www.fake-example.com

This pretty easy, if you are going to be flipping this on/off I would suggest an app like Gas Mask or HostBuddy.

Start your application on port 3000

This is something you’ll have to figure out yourself.

Hit you application in the browser.

Now it’s time to open the application in the browser and test the whole thing. Go ahead and open up https://www.fake-example.com in the browser. If we’ve done everything right, we should see a warning

What the?!

Chrome warning

This is a good thing, Chrome doesn’t trust our self signed certification, so now we just tell Chrome to trust it by clicking Advanced > Proceed to www.fake-example.com (unsafe))

If you get a 502 Bad Gateway it means that Nginx cannot reach your app on port 3000. Most likely because there is a problem with your container talking to your host. Remember I said it’s not as easy as it seems :(

Conclusion

So now you have a local HTTPS server that imitates a production one. It’s now possible to do any type of development locally in your app that might require an HTTPS connection, like Apple Pay for the Web.

If your company needs help implementing Apple Pay for the Web, please contact me. I contract out my services and can have your site accepting Apple Pay payments in short order.