Testing HTTPS Locally With Nanobox

About two months ago, we published an article on using ngrok to share your app on the public Internet. This is still the simplest way to test HTTPS logic in your apps during development (though dry-run deployments do support HTTPS), and it's the best way to share your app before deploying it. But sometimes it's not the best option for everything.

In some cases, you can't use ngrok because you don't have access to the public Internet — being on a plane, for example, or otherwise too far from WiFi or other Internet sources. In others, you have access, but it's not reliable enough to use external services to test your apps — such as at conferences or in hotels where the WiFi is overloaded. In many other cases, your app isn't permitted to be publicly Internet-accessible during development at all, even with access controls and/or random domain naming in place to limit exposure. And so on.

Luckily, there is another option. Put simply, you just need to set up a local web server on your host to proxy requests to your app. While documentation already exists for how to do this, more or less, using whichever web server you prefer, I'm going to detail how to approach this specifically with Nanobox in mind, using Nginx as the proxy server. By the end of this article, you should be able to set up and take down an Nginx proxy in front of any app on your system.

Preparing Your System

Before we get started, a quick note to Windows users. You will need to install the Ubuntu app from the Windows Store, and run the following commands inside that app in order to make these instructions work. A future revision of this article may include instructions for other approaches (such as running these on older versions of Windows). Of course, you're more than welcome to adapt the process given here for use in Windows without waiting for us to get around to it. Hopefully, you'll share your results if you do!

Setting Up Nginx

Obviously, the first step is to install Nginx. Full instructions for that are well beyond the scope of this article, as they're extremely OS-dependent, but luckily the Nginx Wiki has an entry that covers the process for all supported systems. So we'll continue on from here assuming you already have Nginx installed.

You'll also need OpenSSL, so be sure to grab and install that, too. Most *NIX systems come with OpenSSL pre-installed, but Windows specifically does not. You can find ready-to-use versions at the OpenSSL Wiki.

The next step is to create a base Nginx configuration. In many cases, the configuration that ships with your Nginx installer will be a good place to start, and it may even be set up for hosting multiple sites already, though it will most likely be geared toward hosting local files and folders, rather than proxying requests to other servers. Either way, we're going to create our own space for holding Nanobox proxy configurations, partly to keep them separated clearly from other configurations, and partly for consistency across systems.

If your installation of Nginx has a folder for extra configuration files (usually conf.d or similar), separate from the one(s) for virtual hosts (usually either sites-available [with references from sites-enabled to turn them on and off], or vhosts), create a new file in there (that is, in conf.d or similar) called nanobox.conf and insert the following lines in there. If your installation doesn't have such a directory, add these lines to your main nginx.conf instead, inside the http {} block (go ahead and add one if it doesn't already exist, but it probably does):

proxy_hide_header Server;
proxy_hide_header X-Powered-By;
proxy_hide_header X-AspNet-Version;
proxy_set_header  Host              $http_host;
proxy_set_header  X-Forwarded-By    $server_addr;
proxy_set_header  X-Forwarded-For   $remote_addr;
proxy_set_header  X-Forwarded-Proto $scheme;

include nanobox/*;

Note: We want to use the conf.d approach — if possible — because it is the most resilient during software upgrades. Package managers aren't easily able to merge user changes with package changes to config files, so we want to use files that aren't included in the Nginx package itself.

Now, in the same directory as your nginx.conf, create a subdirectory called nanobox/ — this is where we'll place our configs to actually proxy requests to our apps. In that same directory, next to nanobox/, create a file called nanobox.app — we'll use this file to hold the template we'll be using to generate the app configs that will go in nanobox/. Go ahead and add the following lines to nanobox.app:

server {
  listen 80;
  listen 443 ssl;
  server_name {DOMAIN} *.{DOMAIN};

  ssl_certificate     /etc/ssl/nanobox/{DOMAIN}.crt;
  ssl_certificate_key /etc/ssl/nanobox/{DOMAIN}.key;

  location / {
    proxy_pass http://{TARGET};
  }
}

Note: In Windows, change /etc/ssl to the path where you want to store your certificates before saving. I'm going to use C:\ssl in this article, but you'll want to use the actual path on your system. Also note that the Nginx config requires you to use forward slashes (/) in paths instead of backslashes (\), even on Windows.

When you're done, you should have something like this directory structure:

nginx/
  conf.d/
    nanobox.conf
  nanobox/
  nanobox.app
  nginx.conf

Creating A Base Certificate

Create /etc/ssl/nanobox/nanoapp.local.cnf (or C:\ssl\nanobox\nanoapp.local.cnf [for example] in Windows) with the following contents (adjust the *_default values to whatever's appropriate for you and your apps, but leave the {DOMAIN} reference somewhere in the commonName_Default line, because we'll need it later to avoid certificate collisions):

[ req ]
default_bits        = 2048
distinguished_name  = subject
req_extensions      = req_ext
x509_extensions     = x509_ext
string_mask         = utf8only

[ subject ]
countryName         = Country Name (2 letter code)
countryName_default = US

stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = AP

localityName        = Locality Name (eg, city)
localityName_default = Any Town

organizationName    = Organization Name (eg, company)
organizationName_default = Your Company Name

commonName          = Common Name (YOUR name)
commonName_default  = Nanobox Development Server ({DOMAIN})

emailAddress        = Email Address
emailAddress_default = webmaster@example.com

# used when generating a self-signed certificate
[ x509_ext ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer

basicConstraints    = CA:TRUE
keyUsage            = keyCertSign, cRLSign
nsComment           = "Nanobox Development CA Certificate"

# used when generating a CSR
[ req_ext ]
subjectKeyIdentifier = hash

basicConstraints    = CA:FALSE
keyUsage            = digitalSignature, keyEncipherment
subjectAltName      = @alternate_names
nsComment           = "Nanobox Development Server Certificate"

[ alternate_names ]
DNS.1       = {DOMAIN}
DNS.2       = *.{DOMAIN}

Now we'll use this configuration to generate our base certificate (good for 10 years):

sed 's/{DOMAIN}/CA/g' /etc/ssl/nanobox/nanoapp.local.cnf |
  sudo openssl req -x509 -days 3653 -newkey rsa:4096 \
  -batch -sha256 -nodes -config /dev/stdin \
  -keyout /etc/ssl/nanobox/nanoapp-base.key \
  -out /etc/ssl/nanobox/nanoapp-base.crt

The resulting directory should look something like this:

ssl/
  nanobox/
    nanoapp-base.crt
    nanoapp-base.key
    nanoapp.local.cnf

Finally, you'll probably want to import the base cert (nanoapp-base.crt) into your browser, so that it will automatically trust all the app certificates you'll create later. Full instructions on how to do that vary by both browser and OS, so are beyond the scope of this article, but thankfully, there are tutorials for this all over the Internet — just look for a recent one.

Adding An App

In a few moments we're going to create a script to handle this process for us, but I wanted to walk through doing it manually, first, so you can see what's going on, and even skip the script if you like. If you're not interested in how this all works, go ahead and skip to the finished script, below.

Internal DNS Alias

The first step when adding a new app to your Nginx proxy is to set up a DNS alias that Nginx can use to find your app internally. We'll set this to a name you're not likely to use directly:

nanobox dns add dry-run my-app.dry-run.nanoapp.local

Note that we put dry-run in the domain name. This is so you can proxy both your development and dry-run environments simultaneously. If you wanted to set up a proxy for your development environment instead, you'd use this command:

nanobox dns add local my-app.local.nanoapp.local

Notice that we've used .nanoapp.local here — this was chosen intentionally to mirror the .nanoapp.io CNAME domain given to every production app.

Note: These instructions include support for proxying your dry-run environments, but this is no longer really required to get HTTPS working with dry-run, as it's now built into the dry-run environment itself. You are, of course, free to proxy these environments anyway.

Proxy Configuration

OK, so now that you have set up the internal alias, it's time to copy our template into place and fill it out correctly.

sudo cp /etc/nginx/nanobox.app /etc/nginx/nanobox/example.local
sudo sed -i "s/{DOMAIN}/example.local/g" /etc/nginx/nanobox/example.local
sudo sed -i "s/{TARGET}/my-app.dry-run.nanoapp.local/g" /etc/nginx/nanobox/example.local

And of course, use the actual location of your Nginx configuration files instead of the paths given above, and the actual domain you want to point to your app instead of example.local.

The last thing to do, now, is to set up the domain to point to your Nginx server. Open /etc/hosts (or %WinDir%\System32\drivers\etc\hosts on Windows) and add the following line to it. It can go anywhere in the file, but we recommend near the bottom to make it easy to find later.

127.0.1.1       example.local *.example.local

Note: You'll need admin rights to save changes to your hosts file.

Certificates For SSL

In order for the HTTPS portion of all this to work, the certificates have to actually exist. We created a base certificate, above, to handle one part of it. Now to handle the rest.

sed "s/{DOMAIN}/example.local/g" /etc/ssl/nanobox/nanoapp.local.cnf |
  sudo openssl req -newkey rsa:2048 \
  -batch -sha256 -nodes -config /dev/stdin \
  -keyout /etc/ssl/nanobox/example.local.key \
  -out /etc/ssl/nanobox/example.local.csr
sed "s/{DOMAIN}/example.local/g" /etc/ssl/nanobox/nanoap.local.cnf |
  sudo openssl x509 -req -days 365 -CAcreateserial \
  -trustout -extfile /dev/stdin -extensions req_ext \
  -CAkey /etc/ssl/nanobox/nanoapp-base.key \
  -CA /etc/ssl/nanobox/nanoapp-base.crt \
  -in /etc/ssl/nanobox/example.local.csr \
  -out /etc/ssl/nanobox/example.local.crt
sudo rm /etc/ssl/nanobox/example.local.csr
cat /etc/ssl/nanobox/nanoapp-base.crt |
  sudo tee -a /etc/ssl/nanobox/example.local.crt >/dev/null

This certificate is only valid for one year. You can adjust the number of days, if you like, or just repeat this part of the process in a year or so when/before the certificate expires.

Testing The Result

The last step is to reload the Nginx configuration and test that everything works. First, we can ask Nginx to look the config over for any obvious problems:

sudo nginx -t

If everything is OK, we can restart the server:

sudo nginx -s reload

Go ahead and open http://example.local in your browser and make sure you can see your app. Don't worry if your app automatically redirects to https://example.local — you'll want to test that one next anyway. If both are working, then you've got everything set up correctly, and can move forward with your development and/or testing. If not, something went wrong, above, and you'll have to try it again. If you're still stumped, feel free to comment below.

Removing An App

This part is really easy. All we have to do is remove the files we created above, and reload the server configuration again.

nanobox dns rm dry-run my-app.dry-run.nanoapp.local
sudo sed -i '/example\.local \*\.example\.local/d' /etc/hosts
sudo rm /etc/nginx/nanobox/example.local
sudo rm /etc/ssl/nanobox/example.local*
nginx -s reload

And done!

The Complete Script

The script we're about to build takes all the steps above and combines them into a single file. Because it's designed to work for any and all of your apps, there are some options we need to be able to pass to it to set things up properly, and it has to be run from the root directory of your project (just like the nanobox commands). Be sure to adjust it for any differences between it and your own system!

#!/bin/bash

# Find dependencies
if which nginx >/dev/null
then
  echo -n
else
  echo "Nginx not found. Install it and try again."
  exit
fi

if which openssl >/dev/null
then
  echo -n
else
  echo "OpenSSL not found. Install it and try again."
  exit
fi

nginx_path="$(dirname $(nginx -V |& sed 's: :\n:g' | grep conf-path | sed 's/.*conf-path=//') 2>/dev/null)"
ssl_path=/etc/ssl

while [ ! "$(readlink -en "${nginx_path}")" ]
do
  read -ep "Nginx configuration path not found; Enter it here: " -i "${nginx_path}" nginx_path
  nginx_path="$(readlink -en "${nginx_path}")"
done

while [ ! "$(readlink -en "${ssl_path}")" ]
do
  read -ep "SSL configuration path not found; Enter it here: " -i "${ssl_path}" ssl_path
  ssl_path="$(readlink -en "${ssl_path}")"
done

# Prepare arguments for use
self="${0}"
action="${1}"
environment="${2}"
domain="${3}"
app="$(basename $(pwd))"

# Function to print usage statement
function usage () {
  echo <<-EOF
    Usage: $(basename "${self}") [action] [environment] [domain]
      actions:
        add:     add an alias
        rm:      remove an alias

      environments:
        local:   manage alias for development environment
        dry-run: manage alias for dry-run environment

      domain:    domain alias to add for the current app
EOF
}

# Function to remove domains from the proxy
function remove_domain () {
  # Internal alias
  nanobox dns rm $environment "${app}.${environment}.nanoapp.local"

  # $domain alias
  sudo sed -i "/${domain} \*\.${domain}/d" /etc/hosts

  # Nginx configuration
  sudo rm "${nginx_path}/nanobox/${domain}"

  # SSL certificate
  sudo rm "${ssl_path}/nanobox/${domain}.key"
  sudo rm "${ssl_path}/nanobox/${domain}.crt"
}

# Process arguments
case "$domain" in
  "")
    echo 'ERROR: No domain specified'
    usage
    ;;
  *)
    case "$environment" in
      "local"|"dry-run")
        case "$action" in
          "add")
            sudo -v

            # Internal alias
            nanobox dns add $environment "${app}.${environment}.nanoapp.local"

            # $domain alias
            echo "127.0.0.1       ${domain} *.${domain}" | sudo tee -a /etc/hosts >/dev/null

            # Nginx configuration
            sudo cp "${nginx_path}/nanobox.app" "${nginx_path}/nanobox/${domain}"
            sudo sed -i "s/{DOMAIN}/${domain}/g" "${nginx_path}/nanobox/${domain}"
            sudo sed -i "s/{TARGET}/${app}.${environment}.nanoapp.local/g" "${nginx_path}/nanobox/${domain}"

            # SSL certificate
            sed "s/{DOMAIN}/${domain}/g" "${ssl_path}/nanobox/nanoapp.local.cnf" |
              sudo openssl req -newkey rsa:2048 \
              -batch -sha256 -nodes -config /dev/stdin \
              -keyout "${ssl_path}/nanobox/${domain}.key" \
              -out "${ssl_path}/nanobox/${domain}.csr"
            sed "s/{DOMAIN}/${domain}/g" "${ssl_path}/nanobox/nanoapp.local.cnf" |
              sudo openssl x509 -req -days 365 -CAcreateserial \
              -trustout -extfile /dev/stdin -extensions req_ext \
              -CAkey "${ssl_path}/nanobox/nanoapp-base.key" \
              -CA "${ssl_path}/nanobox/nanoapp-base.crt" \
              -in "${ssl_path}/nanobox/${domain}.csr" \
              -out "${ssl_path}/nanobox/${domain}.crt"
            sudo rm "${ssl_path}/nanobox/${domain}.csr"
            cat "${ssl_path}/nanobox/nanoapp-base.crt" | sudo tee -a "${ssl_path}/nanobox/${domain}.crt" >/dev/null

            # Test and reload configuration
            sudo nginx -t || {
              remove_domain
              echo "Failed to set up alias from ${domain} to ${app} (${environment})!"
              exit
            }
            sudo nginx -s reload

            # Complete!
            echo "Alias established from ${domain} to ${app} (${environment})"
            ;;
          "rm")
            sudo -v
            remove_domain
            echo "Alias from ${domain} to ${app} (${environment}) removed"
            ;;
          *)
            echo 'ERROR: Unknown action'
            usage
            ;;
        esac
        ;;
      *)
        echo 'ERROR: Unknown environment'
        usage
        ;;
    esac
    ;;
esac

Save this script as nanoproxy somewhere in your PATH (/usr/local/bin is usually a good place), and mark it as executable.

Use the script in the root directory of your project (the same as you would for nanobox itself) like so:

nanoproxy < add | rm > < local | dry-run > [domain]

That should be all you need from here on to handle local HTTPS testing for your apps. Let us know what you think of this article, or if you have any ideas for improving the scripts given above!

Posted in Nanobox, Development, DevOps, Nginx

Daniel Hunsaker

Daniel Hunsaker

Author, Father, Programmer, Nut. Dan contributes to so many projects he sometimes gets them mixed up. He'll happily help you out on the Nanobox Slack server when the staff are offline.

@sendoshin Idaho, USA
Read More