Ascent

Hosting Multiple HTTPS Domains from the Same Server with Let's Encrypt and Nginx

This post is a continuation of my original, How to Set Up HTTPS with Let's Encrypt and Nginx. If you have not read that post, i suggest starting there.

HTTPS is increasingly required for websites, as it should be. Chrome now explicitly marks HTTP sites with password or credit card fields as "Not Secure". Over the past year, i have been swapping my client sites over to HTTPS. It turns out, as sys-admin work always does, there are hidden challenges associated with doing this.

A brief overview of HTTPS

We should understand why this post exists before we dive into the details below.

The problem with hosting multiple HTTPS sites from the same server has to do with how HTTPS connections are formed. Secure (read: not pwned) HTTPS runs on TLS. When a TLS connection is formed, we tend to think of it as being formed with a website. That is technically incorrect. TLS connections are connected to a specific IP address. Once you connect to that IP, the server is left to assume which site you wanted. This means you cannot have multiple HTTPS sites hosted from the same IP address.

Enter (and exit) SNI

SNI (Server Name Identification) was a protocol introduced in the early 2000s with the intent of fixing this. It is a method for browsers to pass a domain name to a webserver while setting up a TLS connection. By passing the domain name in the initial handshake, the web server knows which domain you intended to visit, allowing it to serve the correct one.

But while this is widely supported, it is not supported ubiquitously. I've personally had a hell of a time fighting with mobile browsers when relying on SNI. On the other hand, IP addresses are cheap. Like, $1/mo or less, cheap. So buck up and grab an distinct IP for your HTTPS sites. Avoiding the headache of some device/browser combos not working will pay for itself 100 times over.

Adding ip addresses

For starters, get an IP for each domain name you intend to host on HTTPS from the same server. Most hosting platforms like Linode and AWS offer the ability to add IP's for just about nothing.

Once you have those IP's you will need to make sure your web server is recognizing input from them. If you start by running a ping, you will probably see the following output:

$ ping you.new.ip.addr
PING you.new.ip.addr (you.new.ip.addr): 56 data bytes  
Request timeout for icmp_seq 0  
Request timeout for icmp_seq 1  
...

This is because our server is ignoring input from unconfigured IP addresses. To make our server watch from input from those IP's, we need to update our networking interface. This sounds scary but honestly is not too bad. We will start by editing /etc/network/interfaces. You probably have a block listing some information about the stock IP address you server exists on.

# Something like this probably already exists
auto eth0  
iface eth0 inet static  
    address X.X.X.X/24
    gateway X.X.X.1
    dns-nameservers 8.8.8.8 8.8.4.4
    dns-search your.dns.info
    dns-options rotate

To this, we will add information about our new IP addresses to the file, below the eth0 block.

# Add support for our new IP addresses
iface eth0:1 inet static  
    address Y.Y.Y.Y/24
    dns-nameservers 8.8.8.8 8.8.4.4

iface eth0:2 inet static  
    address Z.Z.Z.Z/24
    dns-nameservers 8.8.8.8 8.8.4.4

Get our certs

While my original post on this topic has more detail on this topic, i wanted to expand a bit on the steps here. Using Let's Encrypt, we have a couple of options for retrieving our certs.

# First we need the Let's Encrypt bin
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt  

Once we have the Let's Encrypt package, we can register our domains a few ways:

# Get a cert for a domain (include all subdomains that apply to this file path, including www)
/opt/letsencrypt/letsencrypt-auto certonly --webroot \
  -d 'mysite.com,www.mysite.com' -w /var/www/mysite/public 

# Get a cert for a domain and subdomain with different filesystem paths 
/opt/letsencrypt/letsencrypt-auto certonly --webroot \
  -d 'mysite.com,www.mysite.com' -w /var/www/mysite/public \
  -d 'blog.mysite.com' -w /var/www/mysite_blog/ 

# Get a cert for an entirely new domain name
/opt/letsencrypt/letsencrypt-auto certonly --webroot \
  -d 'newsite.com,www.newsite.com' -w /var/www/newsite/public 

Update our nginx servers

The next change we need to make from the basic HTTPS configuration is to tell nginx to switch from a domain+port identification method to an ip+port identification system. The easiest way (and it hasn't bitten me yet) is to simple change the listen directive in our HTTPS server blocks:

server {  
    # Old method:
    # listen          443 ssl; 

    # Ip-based:
    listen          X.X.X.X:443 ssl;
    server_name:    mysite.com www.mysite.com;

    # ...
}

Auto-renew our certs

Finally, once we start dealing with multiple sites on our server, we really should rely on a service that auto-renews our Let's Encrypt certificates. Doing so manually every 3 months is no fun. Luckily, Let's Encrypt gives us a binary to handle this, and all we need to do is schedule it to run regularly via cron. To do this, let's create a file /etc/letsencrypt/auto_renew.sh. To it, add the following lines, or whatever your flavor of them looks like.

#!/bin/sh
# This script renews all the Let's Encrypt certificates with a validity < 30 days

if ! /opt/letsencrypt/letsencrypt-auto renew > /var/log/letsencrypt/renew.log 2>&1 ; then  
    echo Automated renewal failed:
    cat /var/log/letsencrypt/renew.log
    exit 1
fi  
nginx -t && nginx -s reload  

To schedule this script, sun sudo crontab -e and add the following line:

@daily  /etc/letsencrypt/auto_renew.sh

Privacy and security matter

The techopolitical landscape is evolving quickly and in ways that rarely benefit the privacy or security of our users. When a user becomes a customer, there is an implicit, if not explicit, statement of trust in that interaction. As developers/engineers/system admins, we are responsible for protecting our users.

One of the best ways to do that is to secure the connections your users make with your app. This just is not readily possible without some level of secured connection. Let's Encrypt has been an excellent and much needed step toward lowering the barrier of entry for developers. At this point, the excuses available to us for not securing our customer's sites or interactions are neglect and ignorance.

May this post help solve the latter of those reasons.

tl;dr: Hosting multiple HTTPS domains from the same server is a rabbit hole if you don't know what you are doing. Using Let's Encrypt and Nginx, we can make it easy.

Want more tips on Ruby, teams and development? Join the thousands already here.
Author image
Written by Ben
Denver, CO https://benroux.me
VP of Engineering at MeetMindful. I create products, websites and consult for teams on their process and product. Want to chat? Send me an email.