Blog

Serving Multiple SSL-Encrypted Domains from One Application in Nginx

It's a common pattern for web applications to want to provide each user a unique subsection of the site. It might be a single user information page, or it may be an entirely separate application (think something like Harvest or Slack).

The two most common patterns are:

  • Using a URI segment: mostamazingservice.com/12345/
  • Providing a subdomain: clientsubdomain.mostamazingservice.com/

Neither of these options make life particularly difficult when it comes to managing your SSL certs and routing in nginx. Life is easy.

The conflict

I'll start with a recommendation: If at all possible, say no to allowing your users to serve from their own domains.

If you've been running one of these applications long enough, however, it's very likely you'll run into customers—often larger customers—who want to be able to route their own domain name to serve their own section of the site. For example, client-1-domain.com, client-2-domain.com, etc., all pointing to the same application. This is often necessary in the case of "white-labeling" your application, or allowing your customers to use it with their own brand as if it were an internal application.

Before we go into how to do this, I'll start with a recommendation: if at all possible, say no to allowing your users to serve from their own domains. This sort of custom configuration may be necessary or profitable, so it's not always an option to say "no"; but the cost incurred both in terms of effort and also the increased complexity of the system can impose a large burden on your application and team.

Sometimes, you need to say no. Nicely.

I'm not sorry

However, if you've decided to provide this as an option, let's look into what it will take to add multiple domain names, all SSL-encrypted, to our single application instance on nginx on Ubuntu (and we'll also talk about a few specifics in case you're using Laravel Forge). Our primary goal in this tutorial is to focus on creating a system that is simple and flexible, with tidy configuration files, making the entire setup easier to maintain in the long term.

The Battle Plan

We're going to point every domain to the same server application directory, and place all of the domains under the same SSL cert. In cases where you have a large number of whitelisted domains, we'll look at how to load in several certs cleanly.

To accomplish this, we'll need a SAN Cert (Subject Alternative Name Certificate, commonly called UCC by vendors). This type of certificate allows you to secure multiple domains simultaneously. With nginx we'll be able to direct different domain requests to utilize a specific SSL cert while pointing ALL domains to the same application directory. Afterwards, it'll be up to your application to know which client site is active.

Why would I use multiple certs? SAN certs are wonderful, but there are two reasons you may end up with more than one. First, you'll have to re-validate every domain on a cert every time you add a new one domain to it (but that's not as bad as it sounds; read on to learn more). And second, SAN certs are limited to a certain number of domains (which will depend on your SSL cert provider; it may be anywhere from 5 to 250 to even more.

Prerequisites

  • You are running on an Ubuntu box with nginx 1.8.0 (it may work pre-1.8.0 but we haven't done much testing there)
  • You've added your SSH keys to the server
  • You have sudo access

For this tutorial, our application will be mostamazingservice.com. Our directory naming structure will reflect this.

If you're using a tool like Laravel Forge, the work we're doing here will break Forge's ability to upload an SSL cert from their UI.

Step 1: Generate SAN SSL Certificate(s)

If you aren't up to speed on generating CSRs and requesting/loading SAN SSL Certs, I highly recommend working through these two posts:

We'll be modifying some of the nginx config used, but the rest should get you up to speed.

Note: The second link is particularly helpful in confirming you generated your CSR correctly. Every time you submit the CSR to your SSL Certificate vendor, you'll have to confirm ownership of said domains. If you messed up the CSR and submitted it to your vendor, you'll have to submit again.

Step 2: Build the structure of our new nginx configuration file

Open up your terminal. SSH into your server. Make a backup of the nginx config file of your site. Then, pop open the nginx config file for your domain in your favorite terminal text editor. (I'm going to use nano because it's sure to make Matt Stauffer twitch. <3 you, Matt.)

ssh forge@mostamazingservice.com 
sudo cp /etc/nginx/sites-available/mostamazingservice.com /etc/nginx/sites-available/mostamazingservice.com.backup
sudo nano /etc/nginx/sites-available/mostamazingservice.com

/etc/nginx/sites-available/mostamazingservice.com will likely look something like this (as you can see, we're working with Laravel Forge, but this will work for any nginx server):

server {
    server_name mostamazingservice.com;

    root /home/forge/mostamazingservice.com/public;

    # FORGE SSL (DO NOT REMOVE!)
    # ssl_certificate;
    # ssl_certificate_key;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    index index.html index.htm index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/default-error.log error;

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}

Extracting any shared code

First, let's extract some code we'll want to include in all of our configs and save it in /etc/nginx/includes/.

sudo mkdir /etc/nginx/includes
sudo nano /etc/nginx/includes/mostamazingservice.com-all-servers

Paste the lines from ssl_protocols through the end of the location directive, so mostamazingservice.com-all-servers will look like:

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    index index.html index.htm index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/default-error.log error;

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

Back in /etc/nginx/sites-available/mostamazingservice.com, let's include the file we just created. The file will now look like:

server {
    server_name mostamazingservice.com;

    root /home/forge/mostamazingservice.com/public;

    include /etc/nginx/includes/mostamazingservice.com-all-servers;

    # FORGE SSL (DO NOT REMOVE!)
    # ssl_certificate;
    # ssl_certificate_key;  
}

Adding an HTTPS server definition, and redirecting HTTP to HTTPS

Now, let's add the SSL information and enforce HTTP request to be redirected to SSL. We'll also be removing the # FORGE SSL line since Forge won't be able to update it anymore.

server {
    listen 443 ssl;

    server_name mostamazingservice.com;

    root /home/forge/mostamazingservice.com/public;

    include /etc/nginx/includes/mostamazingservice.com-all-servers;

    ssl_certificate       /etc/nginx/ssl/san-1.mostamazingservice.com/server.crt;
    ssl_certificate_key   /etc/nginx/ssl/san-1.mostamazingservice.com/server.key;
}

server {
    listen    80;
    server_name mostamazingservice.com;
    return    301 https://$host$request_uri;
}

Note: In the port 80 server HTTP redirect declaration, most tutorials use $server_name instead of $host. Using $server_name will result in the server redirecting to the first server listed instead of the one requested. Best to avoid redirecting traffic from one client to another.

In the directory where I'm storing my SSL key and certificate, I also keep a copy of the Open SSL server config (with the alt_names for this cert). This makes regeneration of my CSR when I add/change the domains much less of a challenge to track down and generate.

Serving our application to multiple domains

Now, let's serve our application from multiple domains by listing each in the server_name declaration:

server {
    listen    80;
    server_name 
        mostamazingservice.com
        client-domain-1.com
        client-domain-2.com
        client-domain-3.com;

    return    301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    server_name 
        mostamazingservice.com
        client-domain-1.com
        client-domain-2.com
        client-domain-3.com;

    include /etc/nginx/includes/mostamazingservice.com-all-servers;

    ssl_certificate       /etc/nginx/ssl/san-1.mostamazingservice.com/server.crt;
    ssl_certificate_key   /etc/nginx/ssl/san-1.mostamazingservice.com/server.key;
}

Extracting the domain list to a partial

Let's save ourselves even more duplicate entries and extract the server_name declaration to its own file.

sudo nano /etc/nginx/ssl/san-1-mostamazingservice.com/domains

Remove those lines from /etc/nginx/sites-available/mostamazingservice.com and add them to this new file.

server_name 
  mostamazingservice.com
  client-domain-1.com
  client-domain-2.com
  client-domain-3.com;

And edit the file /etc/nginx/sites-available/mostamazingservice.com to import this file:

server {
    listen    80;
    include /etc/nginx/includes/san-1.mostamazingservice.com/domains;
    return    301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    include /etc/nginx/includes/san-1.mostamazingservice.com/domains;
    include /etc/nginx/includes/mostamazingservice.com-all-servers;

    ssl_certificate       /etc/nginx/ssl/san-1.mostamazingservice.com/server.crt;
    ssl_certificate_key   /etc/nginx/ssl/san-1.mostamazingservice.com/server.key;
}

It's always a good idea to test your settings before you restart nginx. You can do that with nginx -c {configFilePath} -t:

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If that comes back OK, let's restart nginx.

sudo service nginx restart

Adding another cert

When you fill up your first certificate, you'll have to spin up another. Here's what it looks like when we have 2 different certs in the same file.

# SAN 1
server {
    listen    80;
    include /etc/nginx/includes/san-1.mostamazingservice.com/domains;
    return    301 https://$host$request_uri;
}
server {
    listen 443 ssl;

    include /etc/nginx/includes/san-1.mostamazingservice.com/domains;
    include /etc/nginx/includes/mostamazingservice.com-all-servers;

    ssl_certificate       /etc/nginx/ssl/san-1.mostamazingservice.com/server.crt;
    ssl_certificate_key   /etc/nginx/ssl/san-1.mostamazingservice.com/server.key;
}

# SAN 2
server {
    listen    80;
    include /etc/nginx/includes/san-2.mostamazingservice.com/domains;
    return    301 https://$host$request_uri;
}
server {
    listen 443 ssl;

    include /etc/nginx/includes/san-2.mostamazingservice.com/domains;
    include /etc/nginx/includes/mostamazingservice.com-all-servers;

    ssl_certificate       /etc/nginx/ssl/san-2.mostamazingservice.com/server.crt;
    ssl_certificate_key   /etc/nginx/ssl/san-2.mostamazingservice.com/server.key;
}

It's like a sunrise over the mountains shining through a rainbow created from the mist of a waterfall.

Additional Notes

Directory Structure: /etc/nginx/ssl/san-1.mostamazingservice.com/

I've settled on a directory structure that holds all information relating a cert in the same place. Hopefully, that will reduce/eliminate the need for documention to my future self.

sudo ls -al /etc/nginx/ssl/san-1.mostamazingservice.com/
-rw-r--r-- 1 root root  2370 Feb 17 21:39 domains         # server_name list of included domains
-rw-r--r-- 1 root root 13989 Feb 17 21:29 server.cnf      # server config used to generate the CSR w/ alt_names filled
-rw-r--r-- 1 root root  9716 Feb 18 16:39 server.crt      # SSL cert provided by your vendor
-rw-r--r-- 1 root root  4174 Feb 17 21:30 server.csr      # CSR provided to your vendor to generate cert bundle
-rw-r--r-- 1 root root  1675 Feb 17 21:13 server.key      # Private SSH key to be used with server.crt

Client Control of DNS

This configuration will be easiest if you are in control of the DNS of each white-labeled domain. Of course, in most cases this won't be an option. Each time you add a domain to a cert and re-generate it, every domain on that cert will need to be verified by the domain owner. The bigger the SAN cert, the more clients you may have to wrangle to approve the cert. You have a few options for arriving at the best solution to avoid annoying your clients, and to get cheaper rates with larger bundles:

  • Fill smaller certs (1-10 domains) with your uncontrolled domains first, and then move those domains to a bigger cert when you can fill it up
  • Generate a cert for each domain
  • Generate a cert for each domain, but have the client generate their own SSL cert with the CSR you provide

Conclusion—for now

With this configuration, regardless of the method you choose, loading new sites and certs should no longer leave you crying.

In a future post, I'll be talking about a quick method for simplifying the process of confirming to your DNS provider that you do, indeed, control those domains, which will make submitting a CSR to your SSL vendor much simpler.

Cheers ~

×

Got an idea? Let's talk.

Leave us a note here, or give us a call at (312) 448-7405.