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.